Compare commits

..

26 Commits

Author SHA1 Message Date
Darko Draskovic 2b393ad50f MF-237 - Add support for storing messages in MongoDB (#307)
* Add mongodb-writer

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add official mongodb driver

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Move Connect to main.go

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove bson.NewDoc and write msg directly in db

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add MongoDB writer tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update README.md

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add mongodb services compose to addons dir

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs and tests

Refactor code.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Expose MetricsMiddleware to align writers with other services

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add logging middleware

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update load tests version

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-01 15:50:23 +02:00
Dušan Borovčanin 9d7f4544c7 MF-296 - Create docker-compose with InfluxDB and Grafana (#306)
* Add docker-compose

Update README.md accordingly.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Move writer compose files to root docker dir

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove Point from InfluxDB writer config

Update docs.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-01 11:20:44 +02:00
Aleksandar Novaković 781d358f38 Add app prefix to dashflux (#320)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-31 17:24:23 +02:00
Dušan Borovčanin 7b6030d8ad NOISSUE - Rename path veriables in InfluxDB wirter (#315)
* Rename path variables

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update README.md

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix README.md deployment env variables

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename InfluxDB point environment variable

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-05-31 12:28:00 +02:00
Aleksandar Novaković 89e69d132a Fix MQTT client id bug (#319)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-30 13:48:40 +02:00
Aleksandar Novaković 7590578745 Change clients to things in documentation (#318)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-29 16:00:18 +02:00
Aleksandar Novaković 97c2d9c30a NOISSUE - Update dashflux port on kubernetes configuration (#317)
* Update dashflux port on kubernetes configuration

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Remove target port from dashflux config

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-29 10:47:27 +02:00
Aleksandar Novaković c38a032fa5 Add version endpoint to nginx configuration (#316)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-28 19:20:17 +02:00
Aleksandar Novaković 1d75268ffa NOISSUE - Fix channel closing bug in WebSocket adapter (#309)
* Remove unnecessary error checks in ws adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix WebSocket adapter channel closing bug

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-28 12:47:20 +02:00
Aleksandar Novaković ef3627f4ee NOISSUE - Add test suite for WebSocket adapter (#299)
* Add load test for WebSocket adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add new test suite to README file

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-25 11:53:45 +02:00
Dušan Borovčanin 808ac50792 MF-282 - Rename clients to things in Dashflux (#305)
* Rename services, stores, environments and models

Rename services, stores and models to match naming change from `clients` to `things`.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Replace all occurrences of clients with things

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docker-compose and nginx.conf

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename `clients service` to `things service`

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename function and veriable names

Remove other occurences of `clients` word/prefix/sufix and replace it with `things`.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix error sending id

Id field must not be sent in POST request in order to create a new thing.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Disable thing type edit

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Export location header

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docker config to use port 3000

Update docker-compose.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Replace id check with bool variable

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename dialogs

Fix bug when creating channel with no connected things.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-05-24 15:26:03 +02:00
Aleksandar Novaković 4c8aeb395c Fix WebSocket mock (#300)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-23 23:33:15 +02:00
Aleksandar Novaković 64e71edf95 NOISSUE - Add mutex to WebSocket service mock (#294) 2018-05-21 23:12:21 +02:00
Dušan Borovčanin c5a40aeffa NOISSUE - Remove redundant error check (#292) 2018-05-21 20:11:04 +02:00
Aleksandar Novaković 3d134d52a6 NOISSUE - Add MQTT to kubernetes nginx (#293)
* Fix nginx configuration for kubernetes

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add dashflux to nginx configuration

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update kubernetes documentation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update soft links paths to relative

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-21 18:18:13 +02:00
Dušan Borovčanin f0cbc47ee6 MF-236 - Add support for storing messages in InfluxDB (#283) 2018-05-21 16:28:52 +02:00
Dejan Mijić e0e2b2aad0 MF-269 - Replace UUID with BIGSERIAL as primary key (#291) 2018-05-21 12:51:46 +02:00
Aleksandar Novaković bbf2d5560c MF-165 - Add gRPC authorization calls to MQTT adapter (#285) 2018-05-20 09:29:19 +02:00
Dušan Borovčanin 69bfb8668e NOISSUE - Fix potential race in gRPC tests (#290) 2018-05-18 13:27:21 +02:00
Aleksandar Novaković 7043dea81a NOISSUE - Add Identify gRPC method (#288) 2018-05-17 20:17:02 +02:00
Govind KP e04d0aeaff NOISSUE - Drop $PWD from docker-compose (#286) 2018-05-17 14:38:16 +02:00
Dejan Mijić 9c6904d9b3 NOISSUE - Update load tests (#287) 2018-05-17 14:21:08 +02:00
Dejan Mijić 38a107919a MF-268 - Replace JWTs with UUIDs as thing's access keys (#284) 2018-05-16 14:28:41 +02:00
Dejan Mijić 48484210c2 NOISSUE - Rename clients to things (#281) 2018-05-15 17:13:09 +02:00
Aleksandar Novaković ceb630c782 NOISSUE - Emit non-SenML messages (#279) 2018-05-14 22:15:12 +02:00
Dušan Borovčanin 570ebc1c72 NOISSUE - Sync docker compose service ports with k8s settings (#278) 2018-05-14 17:21:06 +02:00
415 changed files with 34753 additions and 7684 deletions
Generated
+49 -13
View File
@@ -58,12 +58,6 @@
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
packages = ["."]
revision = "e137e310a99955be01d60b2237da89153ea40987"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
@@ -82,12 +76,6 @@
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
version = "v0.3.3"
[[projects]]
branch = "master"
name = "github.com/dustin/go-coap"
packages = ["."]
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
[[projects]]
name = "github.com/go-kit/kit"
packages = [
@@ -144,6 +132,16 @@
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
name = "github.com/influxdata/influxdb"
packages = [
"client/v2",
"models",
"pkg/escape"
]
revision = "02d7d4f043b34ecb4e9b2dbec298c6f9450c2a32"
version = "v1.5.2"
[[projects]]
branch = "master"
name = "github.com/kr/logfmt"
@@ -165,6 +163,37 @@
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/mongodb/mongo-go-driver"
packages = [
"bson",
"bson/builder",
"bson/decimal",
"bson/elements",
"bson/objectid",
"core/addr",
"core/auth",
"core/auth/internal/gssapi",
"core/command",
"core/connection",
"core/connstring",
"core/description",
"core/dispatch",
"core/options",
"core/readconcern",
"core/readpref",
"core/result",
"core/tag",
"core/topology",
"core/version",
"core/wiremessage",
"core/writeconcern",
"internal",
"mongo"
]
revision = "29a4d7d5b648e5b0b60e03e3bd71c11703f0c203"
version = "v0.0.5"
[[projects]]
name = "github.com/nats-io/go-nats"
packages = [
@@ -329,6 +358,7 @@
packages = [
"bcrypt",
"blowfish",
"pbkdf2",
"ssh/terminal"
]
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
@@ -347,6 +377,12 @@
]
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
[[projects]]
branch = "master"
name = "golang.org/x/sync"
packages = ["semaphore"]
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
@@ -429,6 +465,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "cce4c6cd08b6213615afc135d8e7d4f255d4ee32b42062e1f3208de246366418"
inputs-digest = "6010ae4c4f91c30b3a63789cc280466ce56d50f387c1e4d11c49df1302f083da"
solver-name = "gps-cdcl"
solver-version = 1
+8 -12
View File
@@ -6,18 +6,10 @@
branch = "master"
name = "github.com/cisco/senml"
[[constraint]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.2.0"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-coap"
[[constraint]]
name = "github.com/go-kit/kit"
version = "0.6.0"
@@ -30,10 +22,6 @@
name = "github.com/golang/protobuf"
version = "1.1.0"
[[constraint]]
name = "github.com/jinzhu/gorm"
version = "1.9.1"
[[constraint]]
branch = "master"
name = "github.com/lib/pq"
@@ -74,6 +62,14 @@
branch = "master"
name = "github.com/rubenv/sql-migrate"
[[constraint]]
name = "github.com/influxdata/influxdb"
version = "1.5.2"
[[constraint]]
name = "github.com/mongodb/mongo-go-driver"
version = "0.0.5"
[prune]
go-tests = true
unused-packages = true
+2 -2
View File
@@ -1,5 +1,5 @@
BUILD_DIR = build
SERVICES = users clients http normalizer ws coap
SERVICES = users things http normalizer ws influxdb mongodb
DOCKERS = $(addprefix docker_,$(SERVICES))
CGO_ENABLED ?= 0
GOOS ?= linux
@@ -33,7 +33,7 @@ $(DOCKERS):
dockers: $(DOCKERS)
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile mqtt
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile .
latest: dockers
for svc in $(SERVICES); do \
-81
View File
@@ -1,81 +0,0 @@
# Clients
Clients service provides an HTTP API for managing platform resources: devices,
applications and channels. Through this API clients are able to do the following
actions:
- provision new clients (i.e. devices & applications)
- create new channels
- "connect" clients into the channels
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|------------------------|------------------------------------------|----------------|
| MF_CLIENTS_DB_HOST | Database host address | localhost |
| MF_CLIENTS_DB_PORT | Database host port | 5432 |
| MF_CLIENTS_DB_USER | Database user | mainflux |
| MF_CLIENTS_DB_PASSWORD | Database password | mainflux |
| MF_CLIENTS_DB | Name of the database used by the service | clients |
| MF_CLIENTS_HTTP_PORT | Clients service HTTP port | 8180 |
| MF_CLIENTS_GRPC_PORT | Clients service gRPC port | 8181 |
| MF_USERS_URL | Users service URL | localhost:8181 |
| MF_CLIENTS_SECRET | String used for signing tokens | clients |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
services:
clients:
image: mainflux/clients:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_CLIENTS_DB_HOST: [Database host address]
MF_CLIENTS_DB_PORT: [Database host port]
MF_CLIENTS_DB_USER: [Database user]
MF_CLIENTS_DB_PASS: [Database password]
MF_CLIENTS_DB: [Name of the database used by the service]
MF_CLIENTS_HTTP_PORT: [Service HTTP port]
MF_CLIENTS_GRPC_PORT: [Service gRPC port]
MF_USERS_URL: [Users service URL]
MF_CLIENTS_SECRET: [String used for signing tokens]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the clients
make clients
# copy binary to bin
make install
# set the environment variables and run the service
MF_CLIENTS_DB_HOST=[Database host address] MF_CLIENTS_DB_PORT=[Database host port] MF_CLIENTS_DB_USER=[Database user] MF_CLIENTS_DB_PASS=[Database password] MF_CLIENTS_DB=[Name of the database used by the service] MF_CLIENTS_HTTP_PORT=[Service HTTP port] MF_CLIENTS_GRPC_PORT=[Service gRPC port] MF_USERS_URL=[Users service URL] MF_CLIENTS_SECRET=[String used for signing tokens] $GOBIN/mainflux-clients
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
-49
View File
@@ -1,49 +0,0 @@
package grpc
import (
"github.com/go-kit/kit/endpoint"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/mainflux/mainflux"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var _ mainflux.ClientsServiceClient = (*grpcClient)(nil)
type grpcClient struct {
canAccess endpoint.Endpoint
}
// NewClient returns new gRPC client instance.
func NewClient(conn *grpc.ClientConn) mainflux.ClientsServiceClient {
endpoint := kitgrpc.NewClient(
conn,
"mainflux.ClientsService",
"CanAccess",
encodeCanAccessRequest,
decodeCanAccessResponse,
mainflux.Identity{},
).Endpoint()
return &grpcClient{endpoint}
}
func (client grpcClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, _ ...grpc.CallOption) (*mainflux.Identity, error) {
res, err := client.canAccess(ctx, accessReq{req.GetToken(), req.GetChanID()})
if err != nil {
return nil, err
}
ar := res.(accessRes)
return &mainflux.Identity{Value: ar.id}, ar.err
}
func encodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(accessReq)
return &mainflux.AccessReq{Token: req.clientKey, ChanID: req.chanID}, nil
}
func decodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.Identity)
return accessRes{res.GetValue(), nil}, nil
}
-2
View File
@@ -1,2 +0,0 @@
// Package grpc contains implementation of clients service gRPC API.
package grpc
-22
View File
@@ -1,22 +0,0 @@
package grpc
import (
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/clients"
context "golang.org/x/net/context"
)
func canAccessEndpoint(svc clients.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(accessReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.CanAccess(req.clientKey, req.chanID)
if err != nil {
return accessRes{"", err}, err
}
return accessRes{id, nil}, nil
}
}
-87
View File
@@ -1,87 +0,0 @@
package grpc_test
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
"github.com/mainflux/mainflux/clients/mocks"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = 8080
token = "token"
wrong = "wrong"
email = "john.doe@email.com"
)
var (
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
channel = clients.Channel{Name: "test"}
)
func newService(tokens map[string]string) clients.Service {
users := mocks.NewUsersService(tokens)
clientsRepo := mocks.NewClientRepository()
channelsRepo := mocks.NewChannelRepository(clientsRepo)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
}
func startGRPCServer(svc clients.Service, port int) {
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
server := grpc.NewServer()
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
go server.Serve(listener)
}
func TestCanAccess(t *testing.T) {
svc := newService(map[string]string{token: email})
startGRPCServer(svc, port)
connectedClientID, _ := svc.AddClient(token, client)
connectedClient, _ := svc.ViewClient(token, connectedClientID)
clientID, _ := svc.AddClient(token, client)
client, _ := svc.ViewClient(token, clientID)
chanID, _ := svc.CreateChannel(token, channel)
svc.Connect(token, chanID, connectedClientID)
usersAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(usersAddr, grpc.WithInsecure())
cli := grpcapi.NewClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
cases := map[string]struct {
clientKey string
chanID string
id string
code codes.Code
}{
"check if connected client can access existing channel": {connectedClient.Key, chanID, connectedClientID, codes.OK},
"check if unconnected client can access existing channel": {client.Key, chanID, "", codes.PermissionDenied},
"check if wrong client can access existing channel": {wrong, chanID, "", codes.PermissionDenied},
"check if connected client can access non-existent channel": {connectedClient.Key, "1", "", codes.InvalidArgument},
}
for desc, tc := range cases {
id, err := cli.CanAccess(ctx, &mainflux.AccessReq{tc.clientKey, tc.chanID})
e, ok := status.FromError(err)
assert.True(t, ok, "OK expected to be true")
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", desc, tc.id, id.GetValue()))
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", desc, tc.code, e.Code()))
}
}
-18
View File
@@ -1,18 +0,0 @@
package grpc
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/clients"
)
type accessReq struct {
clientKey string
chanID string
}
func (req accessReq) validate() error {
if !govalidator.IsUUID(req.chanID) || req.clientKey == "" {
return clients.ErrMalformedEntity
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
package grpc
type accessRes struct {
id string
err error
}
-59
View File
@@ -1,59 +0,0 @@
package grpc
import (
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ mainflux.ClientsServiceServer = (*grpcServer)(nil)
type grpcServer struct {
handler kitgrpc.Handler
}
// NewServer returns new ClientsServiceServer instance.
func NewServer(svc clients.Service) mainflux.ClientsServiceServer {
handler := kitgrpc.NewServer(
canAccessEndpoint(svc),
decodeCanAccessRequest,
encodeCanAccessResponse,
)
return &grpcServer{handler}
}
func (s *grpcServer) CanAccess(ctx context.Context, req *mainflux.AccessReq) (*mainflux.Identity, error) {
_, res, err := s.handler.ServeGRPC(ctx, req)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.Identity), nil
}
func decodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.AccessReq)
return accessReq{req.GetToken(), req.GetChanID()}, nil
}
func encodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(accessRes)
return &mainflux.Identity{Value: res.id}, encodeError(res.err)
}
func encodeError(err error) error {
if err == nil {
return nil
}
switch err {
case clients.ErrMalformedEntity:
return status.Error(codes.InvalidArgument, "received invalid can access request")
case clients.ErrUnauthorizedAccess:
return status.Error(codes.PermissionDenied, "missing or invalid credentials provided")
default:
return status.Error(codes.Internal, "internal server error")
}
}
-2
View File
@@ -1,2 +0,0 @@
// Package http contains implementation of clients service HTTP API.
package http
-610
View File
@@ -1,610 +0,0 @@
package http_test
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mainflux/mainflux/clients"
httpapi "github.com/mainflux/mainflux/clients/api/http"
"github.com/mainflux/mainflux/clients/mocks"
"github.com/stretchr/testify/assert"
)
const (
contentType = "application/json"
invalidEmail = "userexample.com"
email = "user@example.com"
token = "token"
invalidToken = "invalid_token"
wrongID = "123e4567-e89b-12d3-a456-000000000042"
id = "123e4567-e89b-12d3-a456-000000000001"
)
var (
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
channel = clients.Channel{Name: "test"}
)
type testRequest struct {
client *http.Client
method string
url string
contentType string
token string
body io.Reader
}
func (tr testRequest) make() (*http.Response, error) {
req, err := http.NewRequest(tr.method, tr.url, tr.body)
if err != nil {
return nil, err
}
if tr.token != "" {
req.Header.Set("Authorization", tr.token)
}
if tr.contentType != "" {
req.Header.Set("Content-Type", tr.contentType)
}
return tr.client.Do(req)
}
func newService(tokens map[string]string) clients.Service {
users := mocks.NewUsersService(tokens)
clientsRepo := mocks.NewClientRepository()
channelsRepo := mocks.NewChannelRepository(clientsRepo)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
}
func newServer(svc clients.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc)
return httptest.NewServer(mux)
}
func toJSON(data interface{}) string {
jsonData, _ := json.Marshal(data)
return string(jsonData)
}
func TestAddClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := toJSON(client)
invalidData := toJSON(clients.Client{
Type: "foo",
Name: "invalid_client",
Payload: "some_payload",
})
cases := []struct {
desc string
req string
contentType string
auth string
status int
location string
}{
{"add valid client", data, contentType, token, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
{"add client with invalid data", invalidData, contentType, token, http.StatusBadRequest, ""},
{"add client with invalid auth token", data, contentType, invalidToken, http.StatusForbidden, ""},
{"add client with invalid request format", "}", contentType, token, http.StatusBadRequest, ""},
{"add client with empty JSON request", "{}", contentType, token, http.StatusBadRequest, ""},
{"add client with empty request", "", contentType, token, http.StatusBadRequest, ""},
{"add client with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodPost,
url: fmt.Sprintf("%s/clients", ts.URL),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
location := res.Header.Get("Location")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
}
}
func TestUpdateClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := toJSON(client)
invalidData := toJSON(clients.Client{
Type: "foo",
Name: client.Name,
Payload: client.Payload,
})
id, _ := svc.AddClient(token, client)
cases := []struct {
desc string
req string
id string
contentType string
auth string
status int
}{
{"update existing client", data, id, contentType, token, http.StatusOK},
{"update non-existent client", data, wrongID, contentType, token, http.StatusNotFound},
{"update client with invalid id", data, "1", contentType, token, http.StatusNotFound},
{"update client with invalid data", invalidData, id, contentType, token, http.StatusBadRequest},
{"update client with invalid user token", data, id, contentType, invalidToken, http.StatusForbidden},
{"update client with invalid data format", "{", id, contentType, token, http.StatusBadRequest},
{"update client with empty JSON request", "{}", id, contentType, token, http.StatusBadRequest},
{"update client with empty request", "", id, contentType, token, http.StatusBadRequest},
{"update client with missing content type", data, id, "", token, http.StatusUnsupportedMediaType},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodPut,
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
fmt.Println(req.url)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestViewClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
id, _ := svc.AddClient(token, client)
client.ID = id
client.Key = id
data := toJSON(client)
cases := []struct {
desc string
id string
auth string
status int
res string
}{
{"view existing client", id, token, http.StatusOK, data},
{"view non-existent client", wrongID, token, http.StatusNotFound, ""},
{"view client by passing invalid id", "1", token, http.StatusNotFound, ""},
{"view client by passing invalid token", id, invalidToken, http.StatusForbidden, ""},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodGet,
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
body, err := ioutil.ReadAll(res.Body)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
data := strings.Trim(string(body), "\n")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
}
}
func TestListClients(t *testing.T) {
noClientsToken := "no_clients_token"
svc := newService(map[string]string{
token: email,
noClientsToken: "no_clients_user@example.com",
})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := []clients.Client{}
for i := 0; i < 101; i++ {
id, _ := svc.AddClient(token, client)
client.ID = id
client.Key = id
data = append(data, client)
}
clientURL := fmt.Sprintf("%s/clients", ts.URL)
cases := []struct {
desc string
auth string
status int
url string
res []clients.Client
}{
{"get a list of clients", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), data[0:5]},
{"get a list of clients with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
{"get a list of clients with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
{"get a list of clients with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), data[0:5]},
{"get a list of clients with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), data[1:11]},
{"get a list of clients with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), data[0:5]},
{"get a list of clients with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
{"get a list of clients with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), data[0:10]},
{"get a list of clients with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
{"get a list of clients with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=5&limit=e"), nil},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodGet,
url: tc.url,
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var data map[string][]clients.Client
json.NewDecoder(res.Body).Decode(&data)
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
}
}
func TestRemoveClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
id, _ := svc.AddClient(token, client)
cases := []struct {
desc string
id string
auth string
status int
}{
{"delete existing client", id, token, http.StatusNoContent},
{"delete non-existent client", wrongID, token, http.StatusNoContent},
{"delete client with invalid id", "1", token, http.StatusNoContent},
{"delete client with invalid token", id, invalidToken, http.StatusForbidden},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodDelete,
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestCreateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
data := toJSON(channel)
cases := []struct {
desc string
req string
contentType string
auth string
status int
location string
}{
{"create new channel", data, contentType, token, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
{"create new channel with invalid token", data, contentType, invalidToken, http.StatusForbidden, ""},
{"create new channel with invalid data format", "{", contentType, token, http.StatusBadRequest, ""},
{"create new channel with empty JSON request", "{}", contentType, token, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
{"create new channel with empty request", "", contentType, token, http.StatusBadRequest, ""},
{"create new channel with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/channels", ts.URL),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
location := res.Header.Get("Location")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
}
}
func TestUpdateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
updateData := toJSON(map[string]string{
"name": "updated_channel",
})
id, _ := svc.CreateChannel(token, channel)
cases := []struct {
desc string
req string
id string
contentType string
auth string
status int
}{
{"update existing channel", updateData, id, contentType, token, http.StatusOK},
{"update non-existing channel", updateData, wrongID, contentType, token, http.StatusNotFound},
{"update channel with invalid token", updateData, id, contentType, invalidToken, http.StatusForbidden},
{"update channel with invalid id", updateData, "1", contentType, token, http.StatusNotFound},
{"update channel with invalid data format", "}", id, contentType, token, http.StatusBadRequest},
{"update channel with empty JSON object", "{}", id, contentType, token, http.StatusOK},
{"update channel with empty request", "", id, contentType, token, http.StatusBadRequest},
{"update channel with missing content type", updateData, id, "", token, http.StatusUnsupportedMediaType},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPut,
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestViewChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
id, _ := svc.CreateChannel(token, channel)
channel.ID = id
data := toJSON(channel)
cases := []struct {
desc string
id string
auth string
status int
res string
}{
{"view existing channel", id, token, http.StatusOK, data},
{"view non-existent channel", wrongID, token, http.StatusNotFound, ""},
{"view channel with invalid id", "1", token, http.StatusNotFound, ""},
{"view channel with invalid token", id, invalidToken, http.StatusForbidden, ""},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodGet,
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
data, err := ioutil.ReadAll(res.Body)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
body := strings.Trim(string(data), "\n")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
}
}
func TestListChannels(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
channels := []clients.Channel{}
for i := 0; i < 101; i++ {
id, _ := svc.CreateChannel(token, channel)
channel.ID = id
channels = append(channels, channel)
}
channelURL := fmt.Sprintf("%s/channels", ts.URL)
cases := []struct {
desc string
auth string
status int
url string
res []clients.Channel
}{
{"get a list of channels", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
{"get a list of channels with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
{"get a list of channels with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
{"get a list of channels with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
{"get a list of channels with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
{"get a list of channels with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", channelURL, 0, 5), channels[0:5]},
{"get a list of channels with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
{"get a list of channels with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
{"get a list of channels with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
{"get a list of channels with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"), nil},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodGet,
url: tc.url,
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var body map[string][]clients.Channel
json.NewDecoder(res.Body).Decode(&body)
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
}
}
func TestRemoveChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
id, _ := svc.CreateChannel(token, channel)
channel.ID = id
cases := []struct {
desc string
id string
auth string
status int
}{
{"remove existing channel", channel.ID, token, http.StatusNoContent},
{"remove non-existent channel", channel.ID, token, http.StatusNoContent},
{"remove channel with invalid id", wrongID, token, http.StatusNoContent},
{"remove channel with invalid token", channel.ID, "invalidToken", http.StatusForbidden},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodDelete,
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestConnect(t *testing.T) {
otherToken := "other_token"
otherEmail := "other_user@example.com"
svc := newService(map[string]string{
token: email,
otherToken: otherEmail,
})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
clientID, _ := svc.AddClient(token, client)
chanID, _ := svc.CreateChannel(token, channel)
otherClientID, _ := svc.AddClient(otherToken, client)
otherChanID, _ := svc.CreateChannel(otherToken, channel)
cases := []struct {
desc string
chanID string
clientID string
auth string
status int
}{
{"connect existing client to existing channel", chanID, clientID, token, http.StatusOK},
{"connect existing client to non-existent channel", wrongID, clientID, token, http.StatusNotFound},
{"connect client with invalid id to channel", chanID, "1", token, http.StatusNotFound},
{"connect client to channel with invalid id", "1", clientID, token, http.StatusNotFound},
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
{"connect client from owner to channel of other user", otherChanID, clientID, token, http.StatusNotFound},
{"connect client from other user to owner's channel", chanID, otherClientID, token, http.StatusNotFound},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodPut,
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestDisconnnect(t *testing.T) {
otherToken := "other_token"
otherEmail := "other_user@example.com"
svc := newService(map[string]string{
token: email,
otherToken: otherEmail,
})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
clientID, _ := svc.AddClient(token, client)
chanID, _ := svc.CreateChannel(token, channel)
svc.Connect(token, chanID, clientID)
otherClientID, _ := svc.AddClient(otherToken, client)
otherChanID, _ := svc.CreateChannel(otherToken, channel)
svc.Connect(otherToken, otherChanID, otherClientID)
cases := []struct {
desc string
chanID string
clientID string
auth string
status int
}{
{"disconnect connected client from channel", chanID, clientID, token, http.StatusNoContent},
{"disconnect non-connected client from channel", chanID, clientID, token, http.StatusNotFound},
{"disconnect non-existent client from channel", chanID, "1", token, http.StatusNotFound},
{"disconnect client from non-existent channel", "1", clientID, token, http.StatusNotFound},
{"disconnect client from channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
{"disconnect owner's client from someone elses channel", otherChanID, clientID, token, http.StatusNotFound},
{"disconnect other's client from owner's channel", chanID, otherClientID, token, http.StatusNotFound},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodDelete,
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
-139
View File
@@ -1,139 +0,0 @@
package http
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/clients"
)
const maxLimitSize = 100
type apiReq interface {
validate() error
}
type identityReq struct {
key string
}
func (req identityReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
return nil
}
type addClientReq struct {
key string
client clients.Client
}
func (req addClientReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
return req.client.Validate()
}
type updateClientReq struct {
key string
id string
client clients.Client
}
func (req updateClientReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return clients.ErrNotFound
}
return req.client.Validate()
}
type createChannelReq struct {
key string
channel clients.Channel
}
func (req createChannelReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
return nil
}
type updateChannelReq struct {
key string
id string
channel clients.Channel
}
func (req updateChannelReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return clients.ErrNotFound
}
return nil
}
type viewResourceReq struct {
key string
id string
}
func (req viewResourceReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return clients.ErrNotFound
}
return nil
}
type listResourcesReq struct {
key string
offset int
limit int
}
func (req *listResourcesReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
return nil
}
return clients.ErrMalformedEntity
}
type connectionReq struct {
key string
chanID string
clientID string
}
func (req connectionReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.chanID) || !govalidator.IsUUID(req.clientID) {
return clients.ErrNotFound
}
return nil
}
-186
View File
@@ -1,186 +0,0 @@
package http
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
const wrong string = "?"
var (
client = clients.Client{Type: "app"}
channel = clients.Channel{}
)
func TestIdentityReqValidation(t *testing.T) {
cases := map[string]struct {
key string
err error
}{
"non-empty token": {uuid.NewV4().String(), nil},
"empty token": {"", clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := identityReq{tc.key}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
client clients.Client
key string
err error
}{
"valid client addition request": {client, key, nil},
"missing token": {client, "", clients.ErrUnauthorizedAccess},
"wrong client type": {clients.Client{Type: wrong}, key, clients.ErrMalformedEntity},
}
for desc, tc := range cases {
req := addClientReq{
key: tc.key,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
client clients.Client
id string
key string
err error
}{
"valid client update request": {client, id, key, nil},
"non-uuid client ID": {client, wrong, key, clients.ErrNotFound},
"missing token": {client, id, "", clients.ErrUnauthorizedAccess},
"wrong client type": {clients.Client{Type: "invalid"}, id, key, clients.ErrMalformedEntity},
}
for desc, tc := range cases {
req := updateClientReq{
key: tc.key,
id: tc.id,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
channel clients.Channel
key string
err error
}{
"valid channel creation request": {channel, key, nil},
"missing token": {channel, "", clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := createChannelReq{
key: tc.key,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
channel clients.Channel
id string
key string
err error
}{
"valid channel update request": {channel, id, key, nil},
"non-uuid channel ID": {channel, wrong, key, clients.ErrNotFound},
"missing token": {channel, id, "", clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := updateChannelReq{
key: tc.key,
id: tc.id,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewResourceReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
id string
key string
err error
}{
"valid resource viewing request": {id, key, nil},
"missing token": {id, "", clients.ErrUnauthorizedAccess},
"non-uuid resource ID": {wrong, key, clients.ErrNotFound},
}
for desc, tc := range cases {
req := viewResourceReq{tc.key, tc.id}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListResourcesReqValidation(t *testing.T) {
key := uuid.NewV4().String()
value := 10
cases := map[string]struct {
key string
offset int
limit int
err error
}{
"valid listing request": {key, value, value, nil},
"missing token": {"", value, value, clients.ErrUnauthorizedAccess},
"negative offset": {key, -value, value, clients.ErrMalformedEntity},
"zero limit": {key, value, 0, clients.ErrMalformedEntity},
"negative limit": {key, value, -value, clients.ErrMalformedEntity},
"too big limit": {key, value, 20 * value, clients.ErrMalformedEntity},
}
for desc, tc := range cases {
req := listResourcesReq{
key: tc.key,
offset: tc.offset,
limit: tc.limit,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
-145
View File
@@ -1,145 +0,0 @@
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/clients"
)
var _ clients.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc clients.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc clients.Service, counter metrics.Counter, latency metrics.Histogram) clients.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) AddClient(key string, client clients.Client) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "add_client").Add(1)
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.AddClient(key, client)
}
func (ms *metricsMiddleware) UpdateClient(key string, client clients.Client) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_client").Add(1)
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateClient(key, client)
}
func (ms *metricsMiddleware) ViewClient(key string, id string) (clients.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_client").Add(1)
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewClient(key, id)
}
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]clients.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_clients").Add(1)
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListClients(key, offset, limit)
}
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_client").Add(1)
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveClient(key, id)
}
func (ms *metricsMiddleware) CreateChannel(key string, channel clients.Channel) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_channel").Add(1)
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CreateChannel(key, channel)
}
func (ms *metricsMiddleware) UpdateChannel(key string, channel clients.Channel) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_channel").Add(1)
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateChannel(key, channel)
}
func (ms *metricsMiddleware) ViewChannel(key string, id string) (clients.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_channel").Add(1)
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewChannel(key, id)
}
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]clients.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_channels").Add(1)
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListChannels(key, offset, limit)
}
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_channel").Add(1)
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveChannel(key, id)
}
func (ms *metricsMiddleware) Connect(key, chanID, clientID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "connect").Add(1)
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Connect(key, chanID, clientID)
}
func (ms *metricsMiddleware) Disconnect(key, chanID, clientID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "disconnect").Add(1)
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Disconnect(key, chanID, clientID)
}
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "can_access").Add(1)
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CanAccess(key, id)
}
-31
View File
@@ -1,31 +0,0 @@
// Package bcrypt provides a hasher implementation utilising bcrypt.
package bcrypt
import (
"github.com/mainflux/mainflux/clients"
"golang.org/x/crypto/bcrypt"
)
const cost int = 10
var _ clients.Hasher = (*bcryptHasher)(nil)
type bcryptHasher struct{}
// New instantiates a bcrypt-based hasher implementation.
func New() clients.Hasher {
return &bcryptHasher{}
}
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
if err != nil {
return "", err
}
return string(hash), nil
}
func (bh *bcryptHasher) Compare(plain, hashed string) error {
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
}
-44
View File
@@ -1,44 +0,0 @@
package clients
// Channel represents a Mainflux "communication group". This group contains the
// clients that can exchange messages between eachother.
type Channel struct {
ID string `json:"id"`
Owner string `json:"-"`
Name string `json:"name,omitempty"`
Clients []Client `json:"connected,omitempty"`
}
// ChannelRepository specifies a channel persistence API.
type ChannelRepository interface {
// Save persists the channel. Successful operation is indicated by unique
// identifier accompanied by nil error response. A non-nil error is
// returned to indicate operation failure.
Save(Channel) (string, error)
// Update performs an update to the existing channel. A non-nil error is
// returned to indicate operation failure.
Update(Channel) error
// One retrieves the channel having the provided identifier, that is owned
// by the specified user.
One(string, string) (Channel, error)
// All retrieves the subset of channels owned by the specified user.
All(string, int, int) []Channel
// Remove removes the channel having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// HasClient determines whether the client with the provided identifier, is
// "connected" to the specified channel.
HasClient(string, string) bool
}
-53
View File
@@ -1,53 +0,0 @@
package clients
import "strings"
// Client represents a Mainflux client. Each client is owned by one user, and
// it is assigned with the unique identifier and (temporary) access key.
type Client struct {
ID string `json:"id"`
Owner string `json:"-"`
Type string `json:"type"`
Name string `json:"name,omitempty"`
Key string `json:"key"`
Payload string `json:"payload,omitempty"`
}
var clientTypes = map[string]bool{
"app": true,
"device": true,
}
// Validate returns an error if client representation is invalid.
func (c *Client) Validate() error {
if c.Type = strings.ToLower(c.Type); !clientTypes[c.Type] {
return ErrMalformedEntity
}
return nil
}
// ClientRepository specifies a client persistence API.
type ClientRepository interface {
// ID generates new resource identifier.
ID() string
// Save persists the client. Successful operation is indicated by non-nil
// error response.
Save(Client) error
// Update performs an update to the existing client. A non-nil error is
// returned to indicate operation failure.
Update(Client) error
// One retrieves the client having the provided identifier, that is owned
// by the specified user.
One(string, string) (Client, error)
// All retrieves the subset of clients owned by the specified user.
All(string, int, int) []Client
// Remove removes the client having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
}
-3
View File
@@ -1,3 +0,0 @@
// Package clients contains the domain concept definitions needed to support
// Mainflux clients service functionality.
package clients
-74
View File
@@ -1,74 +0,0 @@
// Package jwt provides a JWT identity provider.
package jwt
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/mainflux/mainflux/clients"
)
const (
issuer string = "mainflux"
duration time.Duration = 10 * time.Hour
)
var _ clients.IdentityProvider = (*jwtIdentityProvider)(nil)
type jwtIdentityProvider struct {
secret string
}
// New instantiates a JWT identity provider.
func New(secret string) clients.IdentityProvider {
return &jwtIdentityProvider{secret}
}
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
now := time.Now().UTC()
exp := now.Add(duration)
claims := jwt.StandardClaims{
Subject: id,
Issuer: issuer,
IssuedAt: now.Unix(),
ExpiresAt: exp.Unix(),
}
return idp.jwt(claims)
}
func (idp *jwtIdentityProvider) PermanentKey(id string) (string, error) {
claims := jwt.StandardClaims{
Subject: id,
Issuer: issuer,
IssuedAt: time.Now().UTC().Unix(),
}
return idp.jwt(claims)
}
func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(idp.secret))
}
func (idp *jwtIdentityProvider) Identity(key string) (string, error) {
token, err := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, clients.ErrUnauthorizedAccess
}
return []byte(idp.secret), nil
})
if err != nil {
return "", clients.ErrUnauthorizedAccess
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims["sub"].(string), nil
}
return "", clients.ErrUnauthorizedAccess
}
-144
View File
@@ -1,144 +0,0 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/clients"
)
var _ clients.ChannelRepository = (*channelRepositoryMock)(nil)
const chanID = "123e4567-e89b-12d3-a456-"
type channelRepositoryMock struct {
mu sync.Mutex
counter int
channels map[string]clients.Channel
clients clients.ClientRepository
}
// NewChannelRepository creates in-memory channel repository.
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
return &channelRepositoryMock{
channels: make(map[string]clients.Channel),
clients: repo,
}
}
func (crm *channelRepositoryMock) Save(channel clients.Channel) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter++
channel.ID = fmt.Sprintf("%s%012d", chanID, crm.counter)
crm.channels[key(channel.Owner, channel.ID)] = channel
return channel.ID, nil
}
func (crm *channelRepositoryMock) Update(channel clients.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(channel.Owner, channel.ID)
if _, ok := crm.channels[dbKey]; !ok {
return clients.ErrNotFound
}
crm.channels[dbKey] = channel
return nil
}
func (crm *channelRepositoryMock) One(owner, id string) (clients.Channel, error) {
if c, ok := crm.channels[key(owner, id)]; ok {
return c, nil
}
return clients.Channel{}, clients.ErrNotFound
}
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []clients.Channel {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
channels := make([]clients.Channel, 0)
if offset < 0 || limit <= 0 {
return channels
}
// Since IDs starts from 1, shift everything by one.
first := fmt.Sprintf("%s%012d", chanID, offset+1)
last := fmt.Sprintf("%s%012d", chanID, offset+limit+1)
for k, v := range crm.channels {
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
channels = append(channels, v)
}
}
return channels
}
func (crm *channelRepositoryMock) Remove(owner, id string) error {
delete(crm.channels, key(owner, id))
return nil
}
func (crm *channelRepositoryMock) Connect(owner, chanID, clientID string) error {
channel, err := crm.One(owner, chanID)
if err != nil {
return err
}
client, err := crm.clients.One(owner, clientID)
if err != nil {
return err
}
channel.Clients = append(channel.Clients, client)
return crm.Update(channel)
}
func (crm *channelRepositoryMock) Disconnect(owner, chanID, clientID string) error {
channel, err := crm.One(owner, chanID)
if err != nil {
return err
}
if !crm.HasClient(chanID, clientID) {
return clients.ErrNotFound
}
connected := make([]clients.Client, len(channel.Clients)-1)
for _, client := range channel.Clients {
if client.ID != clientID {
connected = append(connected, client)
}
}
channel.Clients = connected
return crm.Update(channel)
}
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
suffix := fmt.Sprintf("-%s", channel)
for k, v := range crm.channels {
if strings.HasSuffix(k, suffix) {
for _, c := range v.Clients {
if c.ID == client {
return true
}
}
break
}
}
return false
}
-94
View File
@@ -1,94 +0,0 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/clients"
)
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
const cliID = "123e4567-e89b-12d3-a456-"
type clientRepositoryMock struct {
mu sync.Mutex
counter int
clients map[string]clients.Client
}
// NewClientRepository creates in-memory client repository.
func NewClientRepository() clients.ClientRepository {
return &clientRepositoryMock{
clients: make(map[string]clients.Client),
}
}
func (crm *clientRepositoryMock) ID() string {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter++
return fmt.Sprintf("%s%012d", cliID, crm.counter)
}
func (crm *clientRepositoryMock) Save(client clients.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.clients[key(client.Owner, client.ID)] = client
return nil
}
func (crm *clientRepositoryMock) Update(client clients.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(client.Owner, client.ID)
if _, ok := crm.clients[dbKey]; !ok {
return clients.ErrNotFound
}
crm.clients[dbKey] = client
return nil
}
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
if c, ok := crm.clients[key(owner, id)]; ok {
return c, nil
}
return clients.Client{}, clients.ErrNotFound
}
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []clients.Client {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
clients := make([]clients.Client, 0)
if offset < 0 || limit <= 0 {
return clients
}
// Since IDs start from 1, shift everything by one.
first := fmt.Sprintf("%s%012d", cliID, offset+1)
last := fmt.Sprintf("%s%012d", cliID, offset+limit+1)
for k, v := range crm.clients {
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
clients = append(clients, v)
}
}
return clients
}
func (crm *clientRepositoryMock) Remove(owner, id string) error {
delete(crm.clients, key(owner, id))
return nil
}
-54
View File
@@ -1,54 +0,0 @@
package mocks
import (
"github.com/mainflux/mainflux/clients"
)
var (
_ clients.Hasher = (*hasherMock)(nil)
_ clients.IdentityProvider = (*identityProviderMock)(nil)
)
type hasherMock struct{}
func (hm *hasherMock) Hash(pwd string) (string, error) {
return pwd, nil
}
func (hm *hasherMock) Compare(plain, hashed string) error {
if plain != hashed {
return clients.ErrUnauthorizedAccess
}
return nil
}
type identityProviderMock struct{}
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
if id == "" {
return "", clients.ErrUnauthorizedAccess
}
return id, nil
}
func (idp *identityProviderMock) PermanentKey(id string) (string, error) {
return idp.TemporaryKey(id)
}
func (idp *identityProviderMock) Identity(key string) (string, error) {
return idp.TemporaryKey(key)
}
// NewHasher creates "no-op" hasher for test purposes. This implementation will
// return secrets without changing them.
func NewHasher() clients.Hasher {
return &hasherMock{}
}
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
// token will hold value provided by the caller.
func NewIdentityProvider() clients.IdentityProvider {
return &identityProviderMock{}
}
-179
View File
@@ -1,179 +0,0 @@
package postgres
import (
"database/sql"
"fmt"
"github.com/lib/pq"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/logger"
uuid "github.com/satori/go.uuid"
)
var _ clients.ChannelRepository = (*channelRepository)(nil)
const (
errDuplicate = "unique_violation"
errFK = "foreign_key_violation"
)
type channelRepository struct {
db *sql.DB
log logger.Logger
}
// NewChannelRepository instantiates a PostgreSQL implementation of channel
// repository.
func NewChannelRepository(db *sql.DB, log logger.Logger) clients.ChannelRepository {
return &channelRepository{db: db, log: log}
}
func (cr channelRepository) Save(channel clients.Channel) (string, error) {
channel.ID = uuid.NewV4().String()
q := `INSERT INTO channels (id, owner, name) VALUES ($1, $2, $3)`
_, err := cr.db.Exec(q, channel.ID, channel.Owner, channel.Name)
if err != nil {
return "", err
}
return channel.ID, nil
}
func (cr channelRepository) Update(channel clients.Channel) error {
q := `UPDATE channels SET name = $1 WHERE owner = $2 AND id = $3;`
res, err := cr.db.Exec(q, channel.Name, channel.Owner, channel.ID)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return clients.ErrNotFound
}
return nil
}
func (cr channelRepository) One(owner, id string) (clients.Channel, error) {
q := `SELECT name FROM channels WHERE id = $1 AND owner = $2`
channel := clients.Channel{ID: id, Owner: owner}
if err := cr.db.QueryRow(q, id, owner).Scan(&channel.Name); err != nil {
empty := clients.Channel{}
if err == sql.ErrNoRows {
return empty, clients.ErrNotFound
}
return empty, err
}
qr := `SELECT id, type, name, key, payload FROM clients cli
INNER JOIN connections conn
ON cli.id = conn.client_id AND cli.owner = conn.client_owner
WHERE conn.channel_id = $1 AND conn.channel_owner = $2`
rows, err := cr.db.Query(qr, id, owner)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return clients.Channel{}, err
}
defer rows.Close()
for rows.Next() {
c := clients.Client{Owner: owner}
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
return clients.Channel{}, err
}
channel.Clients = append(channel.Clients, c)
}
return channel, nil
}
func (cr channelRepository) All(owner string, offset, limit int) []clients.Channel {
q := `SELECT id, name FROM channels WHERE owner = $1 LIMIT $2 OFFSET $3`
items := []clients.Channel{}
rows, err := cr.db.Query(q, owner, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve channels due to %s", err))
return []clients.Channel{}
}
defer rows.Close()
for rows.Next() {
c := clients.Channel{Owner: owner}
if err = rows.Scan(&c.ID, &c.Name); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channel due to %s", err))
return []clients.Channel{}
}
items = append(items, c)
}
return items
}
func (cr channelRepository) Remove(owner, id string) error {
q := `DELETE FROM channels WHERE id = $1 AND owner = $2`
cr.db.Exec(q, id, owner)
return nil
}
func (cr channelRepository) Connect(owner, chanID, clientID string) error {
q := `INSERT INTO connections (channel_id, channel_owner, client_id, client_owner) VALUES ($1, $2, $3, $2)`
if _, err := cr.db.Exec(q, chanID, owner, clientID); err != nil {
pqErr, ok := err.(*pq.Error)
if ok && errFK == pqErr.Code.Name() {
return clients.ErrNotFound
}
// connect is idempotent
if ok && errDuplicate == pqErr.Code.Name() {
return nil
}
return err
}
return nil
}
func (cr channelRepository) Disconnect(owner, chanID, clientID string) error {
q := `DELETE FROM connections
WHERE channel_id = $1 AND channel_owner = $2
AND client_id = $3 AND client_owner = $2`
res, err := cr.db.Exec(q, chanID, owner, clientID)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return clients.ErrNotFound
}
return nil
}
func (cr channelRepository) HasClient(chanID, clientID string) bool {
q := "SELECT EXISTS (SELECT 1 FROM connections WHERE channel_id = $1 AND client_id = $2);"
exists := false
if err := cr.db.QueryRow(q, chanID, clientID).Scan(&exists); err != nil {
cr.log.Error(fmt.Sprintf("Failed to check client existence due to %s", err))
}
return exists
}
-215
View File
@@ -1,215 +0,0 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/postgres"
"github.com/stretchr/testify/assert"
)
func TestChannelSave(t *testing.T) {
email := "channel-save@example.com"
channel := clients.Channel{Owner: email}
channelRepo := postgres.NewChannelRepository(db, testLog)
_, err := channelRepo.Save(channel)
hasErr := err != nil
assert.False(t, hasErr, fmt.Sprintf("create new channel: expected false got %t", hasErr))
}
func TestChannelUpdate(t *testing.T) {
email := "channel-update@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
c := clients.Channel{Owner: email}
id, _ := chanRepo.Save(c)
c.ID = id
cases := map[string]struct {
channel clients.Channel
err error
}{
"existing channel": {c, nil},
"non-existing channel with existing user": {clients.Channel{ID: wrong, Owner: email}, clients.ErrNotFound},
"non-existing channel with non-existing user": {clients.Channel{ID: wrong, Owner: wrong}, clients.ErrNotFound},
}
for desc, tc := range cases {
err := chanRepo.Update(tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleChannelRetrieval(t *testing.T) {
email := "channel-single-retrieval@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
c := clients.Channel{Owner: email}
id, _ := chanRepo.Save(c)
cases := map[string]struct {
owner string
ID string
err error
}{
"existing user": {c.Owner, id, nil},
"existing user, non-existing channel": {c.Owner, wrong, clients.ErrNotFound},
"non-existing owner": {wrong, id, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := chanRepo.One(tc.owner, tc.ID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMultiChannelRetrieval(t *testing.T) {
email := "channel-multi-retrieval@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
n := 10
for i := 0; i < n; i++ {
c := clients.Channel{Owner: email}
chanRepo.Save(c)
}
cases := map[string]struct {
owner string
offset int
limit int
size int
}{
"existing owner": {email, 0, n, n},
"non-existing owner": {wrong, 1, 6, 0},
}
for desc, tc := range cases {
size := len(chanRepo.All(tc.owner, tc.offset, tc.limit))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
}
}
func TestChannelRemoval(t *testing.T) {
email := "channel-removal@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
// show that the removal works the same for both existing and non-existing
// (removed) channel
for i := 0; i < 2; i++ {
if err := chanRepo.Remove(email, chanID); err != nil {
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
}
if _, err := chanRepo.One(email, chanID); err != clients.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
}
}
}
func TestChannelConnect(t *testing.T) {
email := "channel-connect@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
cases := []struct {
desc string
owner string
chanID string
clientID string
err error
}{
{"existing user, channel and client", email, chanID, client.ID, nil},
{"connected channel and client", email, chanID, client.ID, nil},
{"with non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
}
for _, tc := range cases {
err := chanRepo.Connect(tc.owner, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChannelDisconnect(t *testing.T) {
email := "channel-disconnect@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
chanRepo.Connect(email, chanID, client.ID)
cases := []struct {
desc string
owner string
chanID string
clientID string
err error
}{
{"connected client", email, chanID, client.ID, nil},
{"non-connected client", email, chanID, client.ID, clients.ErrNotFound},
{"non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
}
for _, tc := range cases {
err := chanRepo.Disconnect(tc.owner, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChannelAccessCheck(t *testing.T) {
email := "channel-access-check@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
chanRepo.Connect(email, chanID, client.ID)
cases := map[string]struct {
chanID string
clientID string
hasAccess bool
}{
"client that has access": {chanID, client.ID, true},
"client without access": {chanID, wrong, false},
"check access to non-existing channel": {wrong, client.ID, false},
}
for desc, tc := range cases {
hasAccess := chanRepo.HasClient(tc.chanID, tc.clientID)
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
}
}
-101
View File
@@ -1,101 +0,0 @@
package postgres
import (
"database/sql"
"fmt"
_ "github.com/lib/pq" // required for DB access
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/logger"
uuid "github.com/satori/go.uuid"
)
var _ clients.ClientRepository = (*clientRepository)(nil)
type clientRepository struct {
db *sql.DB
log logger.Logger
}
// NewClientRepository instantiates a PostgreSQL implementation of client
// repository.
func NewClientRepository(db *sql.DB, log logger.Logger) clients.ClientRepository {
return &clientRepository{db: db, log: log}
}
func (cr clientRepository) ID() string {
return uuid.NewV4().String()
}
func (cr clientRepository) Save(client clients.Client) error {
q := `INSERT INTO clients (id, owner, type, name, key, payload) VALUES ($1, $2, $3, $4, $5, $6)`
_, err := cr.db.Exec(q, client.ID, client.Owner, client.Type, client.Name, client.Key, client.Payload)
return err
}
func (cr clientRepository) Update(client clients.Client) error {
q := `UPDATE clients SET name = $1, payload = $2 WHERE owner = $3 AND id = $4;`
res, err := cr.db.Exec(q, client.Name, client.Payload, client.Owner, client.ID)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return clients.ErrNotFound
}
return nil
}
func (cr clientRepository) One(owner, id string) (clients.Client, error) {
q := `SELECT name, type, key, payload FROM clients WHERE id = $1 AND owner = $2`
client := clients.Client{ID: id, Owner: owner}
err := cr.db.
QueryRow(q, id, owner).
Scan(&client.Name, &client.Type, &client.Key, &client.Payload)
if err != nil {
empty := clients.Client{}
if err == sql.ErrNoRows {
return empty, clients.ErrNotFound
}
return empty, err
}
return client, nil
}
func (cr clientRepository) All(owner string, offset, limit int) []clients.Client {
q := `SELECT id, name, type, key, payload FROM clients WHERE owner = $1 LIMIT $2 OFFSET $3`
items := []clients.Client{}
rows, err := cr.db.Query(q, owner, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve clients due to %s", err))
return []clients.Client{}
}
defer rows.Close()
for rows.Next() {
c := clients.Client{Owner: owner}
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved client due to %s", err))
return []clients.Client{}
}
items = append(items, c)
}
return items
}
func (cr clientRepository) Remove(owner, id string) error {
q := `DELETE FROM clients WHERE id = $1 AND owner = $2`
cr.db.Exec(q, id, owner)
return nil
}
-133
View File
@@ -1,133 +0,0 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/postgres"
"github.com/stretchr/testify/assert"
)
func TestClientSave(t *testing.T) {
email := "client-save@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
hasErr := clientRepo.Save(client) != nil
assert.False(t, hasErr, fmt.Sprintf("create new client: expected false got %t\n", hasErr))
}
func TestClientUpdate(t *testing.T) {
email := "client-update@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
c := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
client clients.Client
err error
}{
"existing client": {c, nil},
"non-existing client with existing user": {clients.Client{ID: wrong, Owner: email}, clients.ErrNotFound},
"non-existing client with non-existing user": {clients.Client{ID: wrong, Owner: wrong}, clients.ErrNotFound},
}
for desc, tc := range cases {
err := clientRepo.Update(tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleClientRetrieval(t *testing.T) {
email := "client-single-retrieval@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
c := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
owner string
ID string
err error
}{
"existing user": {c.Owner, c.ID, nil},
"existing user, non-existing client": {c.Owner, wrong, clients.ErrNotFound},
"non-existing owner": {wrong, c.ID, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := clientRepo.One(tc.owner, tc.ID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMultiClientRetrieval(t *testing.T) {
email := "client-multi-retrieval@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
n := 10
for i := 0; i < n; i++ {
c := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(c)
}
cases := map[string]struct {
owner string
offset int
limit int
size int
}{
"existing owner, retrieve all": {email, 0, n, n},
"existing owner, retrieve subset": {email, 1, 6, 6},
"non-existing owner": {wrong, 1, 6, 0},
}
for desc, tc := range cases {
n := len(clientRepo.All(tc.owner, tc.offset, tc.limit))
assert.Equal(t, tc.size, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, n))
}
}
func TestClientRemoval(t *testing.T) {
email := "client-removal@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
// show that the removal works the same for both existing and non-existing
// (removed) client
for i := 0; i < 2; i++ {
if err := clientRepo.Remove(email, client.ID); err != nil {
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
}
if _, err := clientRepo.One(email, client.ID); err != clients.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
}
}
}
-25
View File
@@ -1,25 +0,0 @@
package clients
// Hasher specifies an API for generating hashes of an arbitrary textual
// content.
type Hasher interface {
// Hash generates the hashed string from plain-text.
Hash(string) (string, error)
// Compare compares plain-text version to the hashed one. An error should
// indicate failed comparison.
Compare(string, string) error
}
// IdentityProvider specifies an API for identity management via security
// tokens.
type IdentityProvider interface {
// TemporaryKey generates the temporary access token.
TemporaryKey(string) (string, error)
// PermanentKey generates the non-expiring access token.
PermanentKey(string) (string, error)
// Identity extracts the entity identifier given its secret key.
Identity(string) (string, error)
}
-265
View File
@@ -1,265 +0,0 @@
package clients
import (
"context"
"errors"
"time"
"github.com/mainflux/mainflux"
)
var (
// ErrConflict indicates usage of the existing email during account
// registration.
ErrConflict = errors.New("email already taken")
// ErrMalformedEntity indicates malformed entity specification (e.g.
// invalid username or password).
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
)
// Service specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// AddClient adds new client to the user identified by the provided key.
AddClient(string, Client) (string, error)
// UpdateClient updates the client identified by the provided ID, that
// belongs to the user identified by the provided key.
UpdateClient(string, Client) error
// ViewClient retrieves data about the client identified with the provided
// ID, that belongs to the user identified by the provided key.
ViewClient(string, string) (Client, error)
// ListClients retrieves data about subset of clients that belongs to the
// user identified by the provided key.
ListClients(string, int, int) ([]Client, error)
// RemoveClient removes the client identified with the provided ID, that
// belongs to the user identified by the provided key.
RemoveClient(string, string) error
// CreateChannel adds new channel to the user identified by the provided key.
CreateChannel(string, Channel) (string, error)
// UpdateChannel updates the channel identified by the provided ID, that
// belongs to the user identified by the provided key.
UpdateChannel(string, Channel) error
// ViewChannel retrieves data about the channel identified by the provided
// ID, that belongs to the user identified by the provided key.
ViewChannel(string, string) (Channel, error)
// ListChannels retrieves data about subset of channels that belongs to the
// user identified by the provided key.
ListChannels(string, int, int) ([]Channel, error)
// RemoveChannel removes the client identified by the provided ID, that
// belongs to the user identified by the provided key.
RemoveChannel(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// CanAccess determines whether the channel can be accessed using the
// provided key and returns client's id.
CanAccess(string, string) (string, error)
}
var _ Service = (*clientsService)(nil)
type clientsService struct {
users mainflux.UsersServiceClient
clients ClientRepository
channels ChannelRepository
hasher Hasher
idp IdentityProvider
}
// New instantiates the clients service implementation.
func New(users mainflux.UsersServiceClient, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
return &clientsService{
users: users,
clients: clients,
channels: channels,
hasher: hasher,
idp: idp,
}
}
func (ms *clientsService) AddClient(key string, client Client) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return "", ErrUnauthorizedAccess
}
client.ID = ms.clients.ID()
client.Owner = res.GetValue()
client.Key, _ = ms.idp.PermanentKey(client.ID)
return client.ID, ms.clients.Save(client)
}
func (ms *clientsService) UpdateClient(key string, client Client) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
client.Owner = res.GetValue()
return ms.clients.Update(client)
}
func (ms *clientsService) ViewClient(key, id string) (Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return Client{}, ErrUnauthorizedAccess
}
return ms.clients.One(res.GetValue(), id)
}
func (ms *clientsService) ListClients(key string, offset, limit int) ([]Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.clients.All(res.GetValue(), offset, limit), nil
}
func (ms *clientsService) RemoveClient(key, id string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.clients.Remove(res.GetValue(), id)
}
func (ms *clientsService) CreateChannel(key string, channel Channel) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return "", ErrUnauthorizedAccess
}
channel.Owner = res.GetValue()
return ms.channels.Save(channel)
}
func (ms *clientsService) UpdateChannel(key string, channel Channel) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
channel.Owner = res.GetValue()
return ms.channels.Update(channel)
}
func (ms *clientsService) ViewChannel(key, id string) (Channel, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return Channel{}, ErrUnauthorizedAccess
}
return ms.channels.One(res.GetValue(), id)
}
func (ms *clientsService) ListChannels(key string, offset, limit int) ([]Channel, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.channels.All(res.GetValue(), offset, limit), nil
}
func (ms *clientsService) RemoveChannel(key, id string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Remove(res.GetValue(), id)
}
func (ms *clientsService) Connect(key, chanID, clientID string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Connect(res.GetValue(), chanID, clientID)
}
func (ms *clientsService) Disconnect(key, chanID, clientID string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Disconnect(res.GetValue(), chanID, clientID)
}
func (ms *clientsService) CanAccess(key, channel string) (string, error) {
client, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if !ms.channels.HasClient(channel, client) {
return "", ErrUnauthorizedAccess
}
return client, nil
}
-342
View File
@@ -1,342 +0,0 @@
package clients_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/mocks"
"github.com/stretchr/testify/assert"
)
const (
wrong = "wrong-value"
email = "user@example.com"
token = "token"
)
var (
client = clients.Client{Type: "app", Name: "test"}
channel = clients.Channel{Name: "test", Clients: []clients.Client{}}
)
func newService(tokens map[string]string) clients.Service {
users := mocks.NewUsersService(tokens)
clientsRepo := mocks.NewClientRepository()
channelsRepo := mocks.NewChannelRepository(clientsRepo)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
}
func TestAddClient(t *testing.T) {
svc := newService(map[string]string{token: email})
cases := map[string]struct {
client clients.Client
key string
err error
}{
"add new app": {clients.Client{Type: "app", Name: "a"}, token, nil},
"add new device": {clients.Client{Type: "device", Name: "b"}, token, nil},
"add client with wrong credentials": {clients.Client{Type: "app", Name: "d"}, wrong, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.AddClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClient(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
cases := map[string]struct {
client clients.Client
key string
err error
}{
"update existing client": {client, token, nil},
"update client with wrong credentials": {client, wrong, clients.ErrUnauthorizedAccess},
"update non-existing client": {clients.Client{ID: "2", Type: "app", Name: "d"}, token, clients.ErrNotFound},
}
for desc, tc := range cases {
err := svc.UpdateClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewClient(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
cases := map[string]struct {
id string
key string
err error
}{
"view existing client": {client.ID, token, nil},
"view client with wrong credentials": {client.ID, wrong, clients.ErrUnauthorizedAccess},
"view non-existing client": {wrong, token, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := svc.ViewClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListClients(t *testing.T) {
svc := newService(map[string]string{token: email})
n := 10
for i := 0; i < n; i++ {
svc.AddClient(token, client)
}
cases := map[string]struct {
key string
offset int
limit int
size int
err error
}{
"list clients": {token, 0, 5, 5, nil},
"list clients 5-10": {token, 5, 10, 5, nil},
"list last client": {token, 9, 10, 1, nil},
"list empty response": {token, 11, 10, 0, nil},
"list offset < 0": {token, -1, 10, 0, nil},
"list limit < 0": {token, 1, -10, 0, nil},
"list limit = 0": {token, 1, 0, 0, nil},
"list clients with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
cl, err := svc.ListClients(tc.key, tc.offset, tc.limit)
size := len(cl)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestRemoveClient(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
cases := map[string]struct {
id string
key string
err error
}{
"remove client with wrong credentials": {client.ID, "?", clients.ErrUnauthorizedAccess},
"remove existing client": {client.ID, token, nil},
"remove removed client": {client.ID, token, nil},
"remove non-existing client": {"?", token, nil},
}
for desc, tc := range cases {
err := svc.RemoveClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
cases := map[string]struct {
channel clients.Channel
key string
err error
}{
"create channel": {clients.Channel{}, token, nil},
"create channel with wrong credentials": {clients.Channel{}, wrong, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CreateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
channel clients.Channel
key string
err error
}{
"update existing channel": {channel, token, nil},
"update channel with wrong credentials": {channel, wrong, clients.ErrUnauthorizedAccess},
"update non-existing channel": {clients.Channel{ID: "2", Name: "test"}, token, clients.ErrNotFound},
}
for desc, tc := range cases {
err := svc.UpdateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
id string
key string
err error
}{
"view existing channel": {channel.ID, token, nil},
"view channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
"view non-existing channel": {wrong, token, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := svc.ViewChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListChannels(t *testing.T) {
svc := newService(map[string]string{token: email})
n := 10
for i := 0; i < n; i++ {
svc.CreateChannel(token, channel)
}
cases := map[string]struct {
key string
offset int
limit int
size int
err error
}{
"list first 5 channels": {token, 0, 5, 5, nil},
"list channels 5-10 channels": {token, 5, 10, 5, nil},
"list last channel": {token, 6, 10, 4, nil},
"list offset < 0": {token, -1, 10, 0, nil},
"list limit < 0": {token, 1, -10, 0, nil},
"list limit = 0": {token, 1, 0, 0, nil},
"list channels with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
ch, err := svc.ListChannels(tc.key, tc.offset, tc.limit)
size := len(ch)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestRemoveChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
id string
key string
err error
}{
"remove channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
"remove existing channel": {channel.ID, token, nil},
"remove removed channel": {channel.ID, token, nil},
"remove non-existing channel": {channel.ID, token, nil},
}
for desc, tc := range cases {
err := svc.RemoveChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestConnect(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
key string
chanID string
clientID string
err error
}{
"connect client": {token, channel.ID, client.ID, nil},
"connect client with wrong credentials": {wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
"connect client to non-existing channel": {token, wrong, client.ID, clients.ErrNotFound},
}
for desc, tc := range cases {
err := svc.Connect(tc.key, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestDisconnect(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
svc.Connect(token, chanID, clientID)
cases := []struct {
desc string
key string
chanID string
clientID string
err error
}{
{"disconnect connected client", token, channel.ID, client.ID, nil},
{"disconnect disconnected client", token, channel.ID, client.ID, clients.ErrNotFound},
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
{"disconnect client from non-existing channel", token, wrong, client.ID, clients.ErrNotFound},
{"disconnect non-existing client", token, channel.ID, wrong, clients.ErrNotFound},
}
for _, tc := range cases {
err := svc.Disconnect(tc.key, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestCanAccess(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
client.Key = clientID
channel.Clients = []clients.Client{client}
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
key string
channel string
err error
}{
"allowed access": {client.Key, channel.ID, nil},
"not-connected cannot access": {"", channel.ID, clients.ErrUnauthorizedAccess},
"access non-existing channel": {client.Key, wrong, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CanAccess(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
-98
View File
@@ -1,98 +0,0 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/api"
"github.com/mainflux/mainflux/coap/nats"
log "github.com/mainflux/mainflux/logger"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
broker "github.com/nats-io/go-nats"
)
const (
defPort int = 5683
defNatsURL string = broker.DefaultURL
defClientsURL string = "localhost:8181"
envPort string = "MF_COAP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envClientsURL string = "MF_CLIENTS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port int
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: defPort,
}
logger := log.New(os.Stdout)
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
pubsub := nats.New(nc, logger)
svc := coap.New(pubsub)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "coap_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "coap_adapter",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%d", cfg.Port)
logger.Info(fmt.Sprintf("CoAP adapter service started, exposed port %d", cfg.Port))
errs <- api.ListenAndServe(svc, cc, p)
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("CoAP adapter terminated: %s", err))
}
+15 -15
View File
@@ -9,36 +9,36 @@ import (
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
adapter "github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/http/api"
"github.com/mainflux/mainflux/http/nats"
log "github.com/mainflux/mainflux/logger"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
broker "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
const (
defPort string = "8180"
defNatsURL string = broker.DefaultURL
defClientsURL string = "localhost:8181"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envClientsURL string = "MF_CLIENTS_URL"
defPort string = "8180"
defNatsURL string = broker.DefaultURL
defThingsURL string = "localhost:8181"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envThingsURL string = "MF_THINGS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port string
ThingsURL string
NatsURL string
Port string
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.New(os.Stdout)
@@ -50,14 +50,14 @@ func main() {
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
conn, err := grpc.Dial(cfg.ThingsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
cc := thingsapi.NewClient(conn)
pub := nats.NewMessagePublisher(nc)
svc := adapter.New(pub)
+139
View File
@@ -0,0 +1,139 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
influxdata "github.com/influxdata/influxdb/client/v2"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/writers"
"github.com/mainflux/mainflux/writers/influxdb"
nats "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
name = "influxdb-writer"
senML = "out.senml"
prefix = "http://"
defNatsURL = nats.DefaultURL
defPort = "8180"
defPointName = "messages"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "8086"
defDBUser = "mainflux"
defDBPass = "mainflux"
envNatsURL = "MF_NATS_URL"
envPort = "MF_INFLUX_WRITER_PORT"
envDBName = "MF_INFLUX_WRITER_DB_NAME"
envDBHost = "MF_INFLUX_WRITER_DB_HOST"
envDBPort = "MF_INFLUX_WRITER_DB_PORT"
envDBUser = "MF_INFLUX_WRITER_DB_USER"
envDBPass = "MF_INFLUX_WRITER_DB_PASS"
)
type config struct {
NatsURL string
Port string
DBName string
DBHost string
DBPort string
DBUser string
DBPass string
}
func main() {
cfg, clientCfg := loadConfigs()
logger := log.New(os.Stdout)
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
client, err := influxdata.NewHTTPClient(clientCfg)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB client: %s", err))
os.Exit(1)
}
defer client.Close()
repo, err := influxdb.New(client, cfg.DBName)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB writer: %s", err))
os.Exit(1)
}
counter, latency := makeMetrics()
repo = writers.LoggingMiddleware(repo, logger)
repo = writers.MetricsMiddleware(repo, counter, latency)
if err := writers.Start(name, nc, logger, repo); err != nil {
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
os.Exit(1)
}
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
go startHTTPService(cfg.Port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
}
func loadConfigs() (config, influxdata.HTTPConfig) {
cfg := config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
DBName: mainflux.Env(envDBName, defDBName),
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
DBUser: mainflux.Env(envDBUser, defDBUser),
DBPass: mainflux.Env(envDBPass, defDBPass),
}
clientCfg := influxdata.HTTPConfig{
Addr: fmt.Sprintf("%s%s:%s", prefix, cfg.DBHost, cfg.DBPort),
Username: cfg.DBUser,
Password: cfg.DBPass,
}
return cfg, clientCfg
}
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "influxdb",
Subsystem: "message_writer",
Name: "request_count",
Help: "Number of database inserts.",
}, []string{"method"})
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "influxdb",
Subsystem: "message_writer",
Name: "request_latency_microseconds",
Help: "Total duration of inserts in microseconds.",
}, []string{"method"})
return counter, latency
}
func startHTTPService(port string, logger log.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Influxdb writer service started, exposed port %s", p))
errs <- http.ListenAndServe(p, influxdb.MakeHandler())
}
+121
View File
@@ -0,0 +1,121 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/writers"
mongodb "github.com/mainflux/mainflux/writers/mongodb"
"github.com/mongodb/mongo-go-driver/mongo"
nats "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
name = "mongodb-writer"
defNatsURL = nats.DefaultURL
defPort = "8180"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "27017"
envNatsURL = "MF_NATS_URL"
envPort = "MF_MONGO_WRITER_PORT"
envDBName = "MF_MONGO_WRITER_DB_NAME"
envDBHost = "MF_MONGO_WRITER_DB_HOST"
envDBPort = "MF_MONGO_WRITER_DB_PORT"
)
type config struct {
NatsURL string
Port string
DBName string
DBHost string
DBPort string
}
func main() {
cfg := loadConfigs()
logger := log.New(os.Stdout)
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
client, err := mongo.Connect(context.Background(), fmt.Sprintf("mongodb://%s:%s", cfg.DBHost, cfg.DBPort), nil)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to database: %s", err))
os.Exit(1)
}
db := client.Database(cfg.DBName)
repo, err := mongodb.New(db)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create MongoDB writer: %s", err))
os.Exit(1)
}
counter, latency := makeMetrics()
repo = writers.LoggingMiddleware(repo, logger)
repo = writers.MetricsMiddleware(repo, counter, latency)
if err := writers.Start(name, nc, logger, repo); err != nil {
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
os.Exit(1)
}
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
go startHTTPService(cfg.Port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("MongoDB writer service terminated: %s", err))
}
func loadConfigs() config {
return config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
DBName: mainflux.Env(envDBName, defDBName),
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
}
}
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "mongodb",
Subsystem: "message_writer",
Name: "request_count",
Help: "Number of database inserts.",
}, []string{"method"})
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "mongodb",
Subsystem: "message_writer",
Name: "request_latency_microseconds",
Help: "Total duration of inserts in microseconds.",
}, []string{"method"})
return counter, latency
}
func startHTTPService(port string, logger log.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Mongodb writer service started, exposed port %s", p))
errs <- http.ListenAndServe(p, mongodb.MakeHandler())
}
+27 -33
View File
@@ -11,14 +11,13 @@ import (
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/api"
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
httpapi "github.com/mainflux/mainflux/clients/api/http"
"github.com/mainflux/mainflux/clients/bcrypt"
"github.com/mainflux/mainflux/clients/jwt"
"github.com/mainflux/mainflux/clients/postgres"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/things"
"github.com/mainflux/mainflux/things/api"
grpcapi "github.com/mainflux/mainflux/things/api/grpc"
httpapi "github.com/mainflux/mainflux/things/api/http"
"github.com/mainflux/mainflux/things/postgres"
"github.com/mainflux/mainflux/things/uuid"
usersapi "github.com/mainflux/mainflux/users/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
@@ -29,20 +28,18 @@ const (
defDBPort = "5432"
defDBUser = "mainflux"
defDBPass = "mainflux"
defDBName = "clients"
defDBName = "things"
defHTTPPort = "8180"
defGRPCPort = "8181"
defUsersURL = "localhost:8181"
defSecret = "clients"
envDBHost = "MF_CLIENTS_DB_HOST"
envDBPort = "MF_CLIENTS_DB_PORT"
envDBUser = "MF_CLIENTS_DB_USER"
envDBPass = "MF_CLIENTS_DB_PASS"
envDBName = "MF_CLIENTS_DB"
envHTTPPort = "MF_CLIENTS_HTTP_PORT"
envGRPCPort = "MF_CLIENTS_GRPC_PORT"
envDBHost = "MF_THINGS_DB_HOST"
envDBPort = "MF_THINGS_DB_PORT"
envDBUser = "MF_THINGS_DB_USER"
envDBPass = "MF_THINGS_DB_PASS"
envDBName = "MF_THINGS_DB"
envHTTPPort = "MF_THINGS_HTTP_PORT"
envGRPCPort = "MF_THINGS_GRPC_PORT"
envUsersURL = "MF_USERS_URL"
envSecret = "MF_CLIENTS_SECRET"
)
type config struct {
@@ -54,7 +51,6 @@ type config struct {
HTTPPort string
GRPCPort string
UsersURL string
Secret string
}
func main() {
@@ -68,7 +64,7 @@ func main() {
conn := connectToUsersService(cfg.UsersURL, logger)
defer conn.Close()
svc := newService(conn, db, cfg.Secret, logger)
svc := newService(conn, db, logger)
errs := make(chan error, 2)
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
@@ -81,7 +77,7 @@ func main() {
}()
err := <-errs
logger.Error(fmt.Sprintf("Clients service terminated: %s", err))
logger.Error(fmt.Sprintf("Things service terminated: %s", err))
}
func loadConfig() config {
@@ -94,7 +90,6 @@ func loadConfig() config {
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
Secret: mainflux.Env(envSecret, defSecret),
}
}
@@ -116,25 +111,24 @@ func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn
return conn
}
func newService(conn *grpc.ClientConn, db *sql.DB, secret string, logger log.Logger) clients.Service {
func newService(conn *grpc.ClientConn, db *sql.DB, logger log.Logger) things.Service {
users := usersapi.NewClient(conn)
clientsRepo := postgres.NewClientRepository(db, logger)
thingsRepo := postgres.NewThingRepository(db, logger)
channelsRepo := postgres.NewChannelRepository(db, logger)
hasher := bcrypt.New()
idp := jwt.New(secret)
idp := uuid.New()
svc := clients.New(users, clientsRepo, channelsRepo, hasher, idp)
svc := things.New(users, thingsRepo, channelsRepo, idp)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "clients",
Namespace: "things",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "clients",
Namespace: "things",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
@@ -143,20 +137,20 @@ func newService(conn *grpc.ClientConn, db *sql.DB, secret string, logger log.Log
return svc
}
func startHTTPServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
func startHTTPServer(svc things.Service, port string, logger log.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Clients service started, exposed port %s", port))
logger.Info(fmt.Sprintf("Things service started, exposed port %s", port))
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
}
func startGRPCServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
func startGRPCServer(svc things.Service, port string, logger log.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
listener, err := net.Listen("tcp", p)
if err != nil {
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
}
server := grpc.NewServer()
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
logger.Info(fmt.Sprintf("Clients gRPC service started, exposed port %s", port))
mainflux.RegisterThingsServiceServer(server, grpcapi.NewServer(svc))
logger.Info(fmt.Sprintf("Things gRPC service started, exposed port %s", port))
errs <- server.Serve(listener)
}
+15 -15
View File
@@ -9,8 +9,8 @@ import (
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
log "github.com/mainflux/mainflux/logger"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
adapter "github.com/mainflux/mainflux/ws"
"github.com/mainflux/mainflux/ws/api"
"github.com/mainflux/mainflux/ws/nats"
@@ -20,25 +20,25 @@ import (
)
const (
defPort = "8180"
defNatsURL = broker.DefaultURL
defClientsURL = "localhost:8181"
envPort = "MF_WS_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envClientsURL = "MF_CLIENTS_URL"
defPort = "8180"
defNatsURL = broker.DefaultURL
defThingsURL = "localhost:8181"
envPort = "MF_WS_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envThingsURL = "MF_THINGS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port string
ThingsURL string
NatsURL string
Port string
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.New(os.Stdout)
@@ -50,14 +50,14 @@ func main() {
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
conn, err := grpc.Dial(cfg.ThingsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
cc := thingsapi.NewClient(conn)
pubsub := nats.New(nc)
svc := adapter.New(pubsub)
svc = api.LoggingMiddleware(svc, logger)
-59
View File
@@ -1,59 +0,0 @@
# Mainflux CoAP Adapter
Mainflux CoAP adapter provides an [CoAP](http://coap.technology/) API for sending messages through the
platform.
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|-----------------------|------------------------|-------------------------|
| MF_COAP_ADAPTER_PORT | adapter listening port | `5683` |
| MF_NATS_URL | NATS instance URL | `nats://localhost:4222` |
| MF_CLIENTS_URL | clients service URL | `http://localhost:8181` |
## Deployment
The service is distributed as Docker container. The following snippet provides
a compose file template that can be used to deploy the service container locally:
```yaml
version: "2"
services:
adapter:
image: mainflux/coap-adapter:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured port]
environment:
MF_COAP_ADAPTER_PORT: [Service HTTP port]
MF_NATS_URL: [NATS instance URL]
MF_CLIENTS_URL: [Manager service URL]
```
Running this service outside of container requires working instance of the NATS service.
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the http
make coap
# copy binary to bin
make install
# set the environment variables and run the service
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-coap
```
## Usage
Since CoAP protocol does not support `Authorization` header (option), in order to send CoAP messages,
client valid key must be present in `Uri-Query` option.
-160
View File
@@ -1,160 +0,0 @@
// Package coap contains the domain concept definitions needed to support
// Mainflux coap adapter service functionality. All constant values are taken
// from RFC, and could be adjusted based on specific use case.
package coap
import (
"errors"
"math"
"sync"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap/nats"
broker "github.com/nats-io/go-nats"
)
const (
responseBackoffMultiplier = 1.5
// AckTimeout is the amount of time to wait for a response.
AckTimeout = int(2 * time.Second)
// MaxRetransmit is the maximum number of times a message will be retransmitted.
MaxRetransmit = 4
)
var (
// ErrFailedMessagePublish indicates that message publishing failed.
ErrFailedMessagePublish = errors.New("failed to publish message")
// ErrFailedSubscription indicates that client couldn't subscribe to specified channel.
ErrFailedSubscription = errors.New("failed to subscribe to a channel")
// ErrFailedConnection indicates that service couldn't connect to message broker.
ErrFailedConnection = errors.New("failed to connect to message broker")
// extracted to avoid recomputation
maxTimeout = int(float64(AckTimeout) * ((math.Pow(2, float64(MaxRetransmit))) - 1) * responseBackoffMultiplier)
)
// Service specifies coap service API.
type Service interface {
mainflux.MessagePublisher
// Subscribes to channel with specified id and adds subscription to
// service map of subscriptions under given ID.
Subscribe(string, string, nats.Channel) error
// Unsubscribe method is used to stop observing resource.
Unsubscribe(string)
// SetTimeout sets timeout to wait CONF messages.
SetTimeout(string, *time.Timer, int) (chan bool, error)
// RemoveTimeout removes timeout when ACK message is received from client
// if timeout existed.
RemoveTimeout(string)
}
var _ Service = (*adapterService)(nil)
type adapterService struct {
pubsub nats.Service
subs map[string]nats.Channel
mu sync.Mutex
}
// New instantiates the CoAP adapter implementation.
func New(pubsub nats.Service) Service {
return &adapterService{
pubsub: pubsub,
subs: make(map[string]nats.Channel),
mu: sync.Mutex{},
}
}
func (svc *adapterService) get(clientID string) (nats.Channel, bool) {
svc.mu.Lock()
obs, ok := svc.subs[clientID]
svc.mu.Unlock()
return obs, ok
}
func (svc *adapterService) put(clientID string, obs nats.Channel) {
svc.mu.Lock()
svc.subs[clientID] = obs
svc.mu.Unlock()
}
func (svc *adapterService) remove(clientID string) {
svc.mu.Lock()
obs, ok := svc.subs[clientID]
if ok {
obs.Closed <- true
delete(svc.subs, clientID)
}
svc.mu.Unlock()
}
func (svc *adapterService) Publish(msg mainflux.RawMessage) error {
if err := svc.pubsub.Publish(msg); err != nil {
switch err {
case broker.ErrConnectionClosed, broker.ErrInvalidConnection:
return ErrFailedConnection
default:
return ErrFailedMessagePublish
}
}
return nil
}
func (svc *adapterService) Subscribe(chanID, clientID string, ch nats.Channel) error {
// Remove entry if already exists.
svc.Unsubscribe(clientID)
if err := svc.pubsub.Subscribe(chanID, ch); err != nil {
return ErrFailedSubscription
}
svc.put(clientID, ch)
return nil
}
func (svc *adapterService) Unsubscribe(clientID string) {
svc.remove(clientID)
}
func (svc *adapterService) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
sub, ok := svc.get(clientID)
if !ok {
return nil, errors.New("observer entry not found")
}
go func() {
for {
select {
case _, ok := <-sub.Timer:
timer.Stop()
if ok {
sub.Notify <- false
}
return
case <-timer.C:
duration *= 2
if duration >= maxTimeout {
timer.Stop()
sub.Notify <- false
svc.Unsubscribe(clientID)
return
}
timer.Reset(time.Duration(duration))
sub.Notify <- true
}
}
}()
return sub.Notify, nil
}
func (svc *adapterService) RemoveTimeout(clientID string) {
if sub, ok := svc.get(clientID); ok {
sub.Timer <- false
}
}
-3
View File
@@ -1,3 +0,0 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
-66
View File
@@ -1,66 +0,0 @@
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
log "github.com/mainflux/mainflux/logger"
)
var _ coap.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc coap.Service
}
// LoggingMiddleware adds logging facilities to the adapter.
func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method publish to channel %s took %s to complete", msg.Channel, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Publish(msg)
}
func (lm *loggingMiddleware) Subscribe(chanID, clientID string, channel nats.Channel) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method subscribe to channel %s took %s to complete", chanID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Subscribe(chanID, clientID, channel)
}
func (lm *loggingMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
return lm.svc.SetTimeout(clientID, timer, duration)
}
func (lm *loggingMiddleware) RemoveTimeout(clientID string) {
lm.svc.RemoveTimeout(clientID)
}
func (lm *loggingMiddleware) Unsubscribe(clientID string) {
defer func(begin time.Time) {
lm.logger.Info(fmt.Sprintf("Method unsubscribe for client %s took %s to complete", clientID, time.Since(begin)))
}(time.Now())
lm.svc.Unsubscribe(clientID)
}
-54
View File
@@ -1,54 +0,0 @@
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
)
var _ coap.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc coap.Service
}
// MetricsMiddleware instruments adapter by tracking request count and latency.
func MetricsMiddleware(svc coap.Service, counter metrics.Counter, latency metrics.Histogram) coap.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
defer func(begin time.Time) {
mm.counter.With("method", "publish").Add(1)
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Publish(msg)
}
func (mm *metricsMiddleware) Subscribe(chanID, clientID string, channel nats.Channel) error {
return mm.svc.Subscribe(chanID, clientID, channel)
}
func (mm *metricsMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
return mm.svc.SetTimeout(clientID, timer, duration)
}
func (mm *metricsMiddleware) RemoveTimeout(clientID string) {
mm.svc.RemoveTimeout(clientID)
}
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
mm.svc.Unsubscribe(clientID)
}
-150
View File
@@ -1,150 +0,0 @@
package api
import (
"context"
"fmt"
"net"
"strings"
"time"
"google.golang.org/grpc/codes"
"github.com/asaskevich/govalidator"
mux "github.com/dereulenspiegel/coap-mux"
gocoap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"google.golang.org/grpc/status"
)
const (
chanID = "id"
keyHeader = "key"
)
func authKey(opt interface{}) (string, error) {
val, ok := opt.(string)
if !ok {
return "", errBadRequest
}
arr := strings.Split(val, "=")
if len(arr) != 2 || strings.ToLower(arr[0]) != keyHeader {
return "", errBadOption
}
return arr[1], nil
}
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher *mainflux.Identity, err error) {
if !govalidator.IsUUID(cid) {
res.Code = gocoap.NotFound
return
}
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
key, err := authKey(msg.Option(gocoap.URIQuery))
if err != nil {
switch err {
case errBadOption:
res.Code = gocoap.BadOption
case errBadRequest:
res.Code = gocoap.BadRequest
}
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
publisher, err = auth.CanAccess(ctx, &mainflux.AccessReq{Token: key, ChanID: cid})
if err != nil {
e, ok := status.FromError(err)
if ok {
switch e.Code() {
case codes.PermissionDenied:
res.Code = gocoap.Forbidden
default:
res.Code = gocoap.ServiceUnavailable
}
return
}
res.Code = gocoap.InternalServerError
}
return
}
func serve(svc coap.Service, conn *net.UDPConn, data []byte, addr *net.UDPAddr, rh gocoap.Handler) {
msg, err := gocoap.ParseMessage(data)
if err != nil {
return
}
res := &gocoap.Message{
Type: gocoap.NonConfirmable,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
switch msg.Type {
case gocoap.Reset:
if len(msg.Payload) != 0 {
res.Code = gocoap.BadRequest
break
}
cid := mux.Var(&msg, chanID)
res.Type = gocoap.Acknowledgement
publisher, err := authorize(&msg, res, cid)
if err != nil {
break
}
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
svc.RemoveTimeout(id)
svc.Unsubscribe(id)
case gocoap.Acknowledgement:
cid := mux.Var(&msg, chanID)
res.Type = gocoap.Acknowledgement
publisher, err := authorize(&msg, res, cid)
if err != nil {
break
}
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
svc.RemoveTimeout(id)
default:
res = rh.ServeCOAP(conn, addr, &msg)
}
if res != nil && msg.IsConfirmable() {
gocoap.Transmit(conn, addr, *res)
}
}
// ListenAndServe binds to the given address and serve requests forever.
func ListenAndServe(svc coap.Service, csc mainflux.ClientsServiceClient, addr string) error {
handler := makeHandler(addr, svc)
auth = csc
uaddr, err := net.ResolveUDPAddr(network, addr)
if err != nil {
return err
}
conn, err := net.ListenUDP(network, uaddr)
if err != nil {
return err
}
buf := make([]byte, maxPktLen)
for {
nr, addr, err := conn.ReadFromUDP(buf)
if err != nil {
if neterr, ok := err.(net.Error); ok && (neterr.Temporary() || neterr.Timeout()) {
time.Sleep(5 * time.Millisecond)
continue
}
return err
}
tmp := make([]byte, nr)
copy(tmp, buf)
go serve(svc, conn, tmp, addr, handler)
}
}
-225
View File
@@ -1,225 +0,0 @@
package api
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
"math/rand"
mux "github.com/dereulenspiegel/coap-mux"
gocoap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
)
var (
errBadRequest = errors.New("bad request")
errBadOption = errors.New("bad option")
auth mainflux.ClientsServiceClient
)
const (
maxPktLen = 1500
network = "udp"
protocol = "coap"
)
const (
// Approximately number of supported requests per second
timestamp = int64(time.Millisecond) * 31
)
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
// NotFoundHandler handles erroneously formed requests.
func NotFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *gocoap.Message) *gocoap.Message {
if m.IsConfirmable() {
return &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.NotFound,
}
}
return nil
}
func version(port string) {
b := bone.New()
b.GetFunc("/version", mainflux.Version("CoAP"))
http.ListenAndServe(port, b)
}
func makeHandler(port string, svc coap.Service) gocoap.Handler {
go version(port)
r := mux.NewRouter()
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(observe(svc))).Methods(gocoap.GET)
r.NotFoundHandler = gocoap.FuncHandler(NotFoundHandler)
return r
}
func receive(svc coap.Service) handler {
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
var res *gocoap.Message
if msg.IsConfirmable() {
res = &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
}
if len(msg.Payload) == 0 && msg.IsConfirmable() {
res.Code = gocoap.BadRequest
return res
}
cid := mux.Var(msg, "id")
publisher, err := authorize(msg, res, cid)
if err != nil {
return res
}
rawMsg := mainflux.RawMessage{
Channel: cid,
Publisher: publisher.GetValue(),
Protocol: protocol,
Payload: msg.Payload,
}
if err := svc.Publish(rawMsg); err != nil {
res.Code = gocoap.InternalServerError
}
return res
}
}
func observe(svc coap.Service) handler {
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
var res *gocoap.Message
if msg.IsConfirmable() {
res = &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
}
cid := mux.Var(msg, "id")
publisher, err := authorize(msg, res, cid)
if err != nil {
return res
}
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 1 {
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
svc.Unsubscribe(id)
}
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
ch := nats.Channel{
Messages: make(chan mainflux.RawMessage),
Closed: make(chan bool),
Timer: make(chan bool),
Notify: make(chan bool),
}
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
if err := svc.Subscribe(cid, id, ch); err != nil {
res.Code = gocoap.InternalServerError
return res
}
go handleSub(svc, id, conn, addr, msg, ch)
res.AddOption(gocoap.Observe, 0)
}
return res
}
}
func sendMessage(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) error {
buff := new(bytes.Buffer)
now := time.Now().UnixNano() / timestamp
if err := binary.Write(buff, binary.BigEndian, now); err != nil {
return err
}
observeVal := buff.Bytes()
msg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
if msg.IsConfirmable() {
timer := time.NewTimer(time.Duration(coap.AckTimeout))
ch, err := svc.SetTimeout(id, timer, coap.AckTimeout)
if err != nil {
return err
}
go sendConfirmable(conn, addr, msg, ch)
return nil
}
return gocoap.Transmit(conn, addr, *msg)
}
func sendConfirmable(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch chan bool) {
msg.SetOption(gocoap.MaxRetransmit, coap.MaxRetransmit)
// Try to transmit MAX_RETRANSMITION times; every attempt duplicates timeout between transmission.
for i := 0; i < coap.MaxRetransmit; i++ {
if err := gocoap.Transmit(conn, addr, *msg); err != nil {
return
}
state, ok := <-ch
if !state || !ok {
return
}
}
return
}
func handleSub(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch nats.Channel) {
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
// 24 hours. Since 24 hours is too long for our purposes, we use 12.
ticker := time.NewTicker(12 * time.Hour)
res := &gocoap.Message{
Type: gocoap.NonConfirmable,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
res.SetOption(gocoap.LocationPath, msg.Path())
for {
select {
case <-ticker.C:
ticker.Stop()
res.Type = gocoap.Confirmable
rand.Seed(time.Now().UnixNano())
if err := sendMessage(svc, id, conn, addr, res); err != nil {
ticker.Stop()
return
}
case rawMsg, ok := <-ch.Messages:
if !ok {
ticker.Stop()
return
}
res.Type = gocoap.NonConfirmable
res.Payload = rawMsg.Payload
if err := sendMessage(svc, id, conn, addr, res); err != nil {
ticker.Stop()
return
}
}
}
}
-53
View File
@@ -1,53 +0,0 @@
// Package nats contains NATS-specific message repository implementation.
package nats
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
broker "github.com/nats-io/go-nats"
)
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
const prefix = "channel"
type natsPublisher struct {
nc *broker.Conn
logger log.Logger
}
// New instantiates NATS message pubsub.
func New(nc *broker.Conn, l log.Logger) Service {
return &natsPublisher{nc, l}
}
func (pubsub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := proto.Marshal(&msg)
if err != nil {
return err
}
return pubsub.nc.Publish(fmt.Sprintf("%s.%s", prefix, msg.Channel), data)
}
func (pubsub *natsPublisher) Subscribe(chanID string, channel Channel) error {
sub, err := pubsub.nc.Subscribe(fmt.Sprintf("%s.%s", prefix, chanID), func(msg *broker.Msg) {
if msg == nil {
return
}
var rawMsg mainflux.RawMessage
if err := proto.Unmarshal(msg.Data, &rawMsg); err != nil {
return
}
channel.Messages <- rawMsg
})
go func() {
<-channel.Closed
sub.Unsubscribe()
channel.Close()
}()
return err
}
-26
View File
@@ -1,26 +0,0 @@
package nats
import "github.com/mainflux/mainflux"
// Service specifies NATS service API.
type Service interface {
mainflux.MessagePublisher
// Subscribe is used to subscribe to channel with specified id.
Subscribe(string, Channel) error
}
// Channel is used for receiving and sending messages.
type Channel struct {
Messages chan mainflux.RawMessage
Closed chan bool
Timer chan bool
Notify chan bool
}
// Close channel and stop message transfer.
func (channel Channel) Close() {
close(channel.Messages)
close(channel.Closed)
close(channel.Timer)
close(channel.Notify)
}
+2 -2
View File
@@ -1,5 +1,5 @@
# Stage 0, based on Node.js, to build and compile Angular
FROM node:8.6 as node
FROM node:9.5 as node
WORKDIR /app
COPY package.json /app/
RUN npm install
@@ -8,6 +8,6 @@ ARG env=prod
RUN npm run build -- --prod --environment $env
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM nginx:1.13
FROM nginx:1.13-alpine
COPY --from=node /app/dist/ /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -1,5 +1,5 @@
server {
listen 80;
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
+3 -3
View File
@@ -6,13 +6,13 @@ import { ProtectedGuard, PublicGuard } from 'ngx-auth';
import { LoginComponent } from './components/auth/login/login.component';
import { SignupComponent } from './components/auth/signup/signup.component';
import { ChannelsComponent } from './components/channels/channels.component';
import { ClientsComponent } from './components/clients/clients.component';
import { ThingsComponent } from './components/things/things.component';
const routes: Route[] = [
{ path: '', redirectTo: 'clients', pathMatch: 'full'},
{ path: '', redirectTo: 'things', pathMatch: 'full'},
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
{ path: 'clients', component: ClientsComponent, canActivate: [ProtectedGuard]},
{ path: 'things', component: ThingsComponent, canActivate: [ProtectedGuard]},
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
];
+2 -2
View File
@@ -5,11 +5,11 @@
<mat-toolbar color="primary" class="mat-elevation-z6">
<h1 fxFlex>Mainflux</h1>
<ng-container *ngIf="authStore.isAuthenticated">
<a [routerLink]="['/clients']" mat-button>Clients</a>
<a [routerLink]="['/things']" mat-button>Things</a>
<a [routerLink]="['/channels']" mat-button>Channels</a>
<a (click)="logout()" mat-button>Logout</a>
</ng-container>
</mat-toolbar>
<router-outlet></router-outlet>
</div>
+2 -2
View File
@@ -7,7 +7,7 @@ import { MaterialModule } from './core/material/material.module';
import { AuthenticationService } from './core/services/auth/authentication.service';
import { TokenStorage } from './core/services/auth/token-storage.service';
import { ChannelsService } from './core/services/channels/channels.service';
import { ClientsService } from './core/services/clients/clients.service';
import { ThingsService } from './core/services/things/things.service';
import { UiStore } from './core/store/ui.store';
import { AuthStore } from './core/store/auth.store';
@@ -27,7 +27,7 @@ describe('AppComponent', () => {
AuthStore,
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
]
}).compileComponents();
+16 -14
View File
@@ -2,6 +2,7 @@ import './rxjs-extensions.ts';
import 'hammerjs';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_BASE_HREF } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
@@ -14,21 +15,21 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './components/auth/login/login.component';
import { SignupComponent } from './components/auth/signup/signup.component';
import { AddChannelDialogComponent } from './components/channels/add-channel-dialog/add-channel-dialog.component';
import { ChannelDialogComponent } from './components/channels/channel-dialog/channel-dialog.component';
import { ChannelsComponent } from './components/channels/channels.component';
import { AddClientDialogComponent } from './components/clients/add-client-dialog/add-client-dialog.component';
import { ClientsComponent } from './components/clients/clients.component';
import { ThingDialogComponent } from './components/things/thing-dialog/thing-dialog.component';
import { ThingsComponent } from './components/things/things.component';
import { ConfirmationDialogComponent } from './components/shared/confirmation-dialog/confirmation-dialog.component';
import { MaterialModule } from './core/material/material.module';
import { AuthenticationService } from './core/services/auth/authentication.service';
import { TokenStorage } from './core/services/auth/token-storage.service';
import { ChannelsService } from './core/services/channels/channels.service';
import { ClientsService } from './core/services/clients/clients.service';
import { ThingsService } from './core/services/things/things.service';
import { MockAuthService } from './core/services/mock-auth.service';
import { MockChannelsService } from './core/services/mock-channels.service';
import { MockClientsService } from './core/services/mock-clients.service';
import { MockThingsService } from './core/services/mock-things.service';
import { ChannelsStore } from './core/store/channels.store';
import { ClientsStore } from './core/store/clients.store';
import { ThingsStore } from './core/store/things.store';
import { UiStore } from './core/store/ui.store';
import { AuthStore } from './core/store/auth.store';
import { UnauthorizedInterceptor } from './core/services/auth/unauthorized.interceptor';
@@ -40,13 +41,13 @@ export function factory(authenticationService: AuthenticationService) {
@NgModule({
declarations: [
AppComponent,
ClientsComponent,
ThingsComponent,
ChannelsComponent,
SignupComponent,
LoginComponent,
AddClientDialogComponent,
ThingDialogComponent,
ConfirmationDialogComponent,
AddChannelDialogComponent,
ChannelDialogComponent,
],
imports: [
AuthModule,
@@ -61,13 +62,13 @@ export function factory(authenticationService: AuthenticationService) {
],
providers: [
UiStore,
ClientsStore,
ThingsStore,
ChannelsStore,
AuthStore,
MockAuthService,
MockClientsService,
MockThingsService,
MockChannelsService,
ClientsService,
ThingsService,
ChannelsService,
TokenStorage,
AuthenticationService,
@@ -79,11 +80,12 @@ export function factory(authenticationService: AuthenticationService) {
useFactory: factory
},
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
{ provide: APP_BASE_HREF, useValue: '/app/' }
],
bootstrap: [AppComponent],
entryComponents: [
AddClientDialogComponent,
AddChannelDialogComponent,
ThingDialogComponent,
ChannelDialogComponent,
ConfirmationDialogComponent
]
})
@@ -10,8 +10,8 @@
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="Connected clients" formControlName="connected" [compareWith]="compareFunction" multiple>
<mat-option *ngFor="let client of clientsStore.clients" [value]="client">{{client.name}}</mat-option>
<mat-select placeholder="Connected things" formControlName="connected" [compareWith]="compareFunction" multiple>
<mat-option *ngFor="let thing of thingsStore.things" [value]="thing">{{thing.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
@@ -19,4 +19,4 @@
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addChannelForm.invalid">Ok</button>
</div>
</form>
</form>
@@ -9,18 +9,18 @@ import { MaterialModule } from '../../../core/material/material.module';
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../../core/services/channels/channels.service';
import { ClientsService } from '../../../core/services/clients/clients.service';
import { AddChannelDialogComponent } from './add-channel-dialog.component';
import { ClientsStore } from '../../../core/store/clients.store';
import { ThingsService } from '../../../core/services/things/things.service';
import { ChannelDialogComponent } from './channel-dialog.component';
import { ThingsStore } from '../../../core/store/things.store';
import { UiStore } from '../../../core/store/ui.store';
describe('AddChannelDialogComponent', () => {
let component: AddChannelDialogComponent;
let fixture: ComponentFixture<AddChannelDialogComponent>;
describe('ChannelDialogComponent', () => {
let component: ChannelDialogComponent;
let fixture: ComponentFixture<ChannelDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddChannelDialogComponent ],
declarations: [ ChannelDialogComponent ],
imports: [
MaterialModule,
MatDialogModule,
@@ -31,11 +31,11 @@ describe('AddChannelDialogComponent', () => {
NoopAnimationsModule
],
providers: [
ClientsStore,
ThingsStore,
UiStore,
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -45,7 +45,7 @@ describe('AddChannelDialogComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddChannelDialogComponent);
fixture = TestBed.createComponent(ChannelDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -3,24 +3,24 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { toJS } from 'mobx';
import { ClientsStore } from '../../../core/store/clients.store';
import { Channel, Client } from '../../../core/store/models';
import { ThingsStore } from '../../../core/store/things.store';
import { Channel, Thing } from '../../../core/store/models';
@Component({
selector: 'app-add-channel-dialog',
templateUrl: './add-channel-dialog.component.html',
styleUrls: ['./add-channel-dialog.component.scss']
selector: 'app-channel-dialog',
templateUrl: './channel-dialog.component.html',
styleUrls: ['./channel-dialog.component.scss']
})
export class AddChannelDialogComponent implements OnInit {
export class ChannelDialogComponent implements OnInit {
addChannelForm: FormGroup;
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
editMode: boolean;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddChannelDialogComponent>,
private dialogRef: MatDialogRef<ChannelDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: Channel,
public clientsStore: ClientsStore,
public thingsStore: ThingsStore,
) { }
ngOnInit() {
@@ -46,7 +46,7 @@ export class AddChannelDialogComponent implements OnInit {
this.dialogRef.close();
}
compareFunction(obj1: Client, obj2: Client) {
compareFunction(obj1: Thing, obj2: Thing) {
return obj1.id === obj2.id;
}
}
@@ -9,9 +9,9 @@ import { MaterialModule } from '../../core/material/material.module';
import { AuthenticationService } from '../../core/services/auth/authentication.service';
import { TokenStorage } from '../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../core/services/channels/channels.service';
import { ClientsService } from '../../core/services/clients/clients.service';
import { ThingsService } from '../../core/services/things/things.service';
import { ChannelsComponent } from './channels.component';
import { ClientsStore } from '../../core/store/clients.store';
import { ThingsStore } from '../../core/store/things.store';
import { UiStore } from '../../core/store/ui.store';
import { ChannelsStore } from '../../core/store/channels.store';
@@ -33,9 +33,9 @@ describe('ChannelsComponent', () => {
],
providers: [
{
provide: ClientsStore,
provide: ThingsStore,
useClass: class {
getClients = jasmine.createSpy('getClients');
getThings = jasmine.createSpy('getThings');
}
},
UiStore,
@@ -47,7 +47,7 @@ describe('ChannelsComponent', () => {
},
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -3,10 +3,10 @@ import { MatDialog } from '@angular/material';
import { Observable } from 'rxjs/Observable';
import { ChannelsStore } from '../../core/store/channels.store';
import { ClientsStore } from '../../core/store/clients.store';
import { ThingsStore } from '../../core/store/things.store';
import { Channel } from '../../core/store/models';
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
import { AddChannelDialogComponent } from './add-channel-dialog/add-channel-dialog.component';
import { ChannelDialogComponent } from './channel-dialog/channel-dialog.component';
@Component({
selector: 'app-channels',
@@ -19,17 +19,17 @@ export class ChannelsComponent implements OnInit {
constructor(
private dialog: MatDialog,
public clientsStore: ClientsStore,
public thingsStore: ThingsStore,
public channelsStore: ChannelsStore,
) { }
ngOnInit() {
this.channelsStore.getChannels();
this.clientsStore.getClients();
this.thingsStore.getThings();
}
addChannel() {
const dialogRef = this.dialog.open(AddChannelDialogComponent);
const dialogRef = this.dialog.open(ChannelDialogComponent);
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
this.channelsStore.addChannel(channel);
@@ -37,7 +37,7 @@ export class ChannelsComponent implements OnInit {
}
editChannel(channel: Channel) {
const dialogRef = this.dialog.open(AddChannelDialogComponent, {
const dialogRef = this.dialog.open(ChannelDialogComponent, {
data: channel
});
@@ -1,43 +0,0 @@
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Client } from '../../../core/store/models';
@Component({
selector: 'app-add-client-dialog',
templateUrl: './add-client-dialog.component.html',
styleUrls: ['./add-client-dialog.component.scss']
})
export class AddClientDialogComponent implements OnInit {
addClientForm: FormGroup;
@Output() submit: EventEmitter<Client> = new EventEmitter<Client>();
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddClientDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: Client
) { }
ngOnInit() {
this.addClientForm = this.fb.group(
{
id: [''],
type: ['', [Validators.required]],
name: ['', [Validators.required, Validators.minLength(5)]],
payload: ['']
}
);
if (this.data) {
this.addClientForm.patchValue(this.data);
this.addClientForm.get('payload').patchValue(JSON.stringify(this.data.payload));
}
}
onAddClient() {
const client = this.addClientForm.value;
this.submit.emit(client);
this.dialogRef.close();
}
}
@@ -1,63 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { toJS } from 'mobx';
import { Observable } from 'rxjs/Observable';
import { Client } from '../../core/store/models';
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
import { AddClientDialogComponent } from './add-client-dialog/add-client-dialog.component';
import { ClientsStore } from '../../core/store/clients.store';
import { ChannelsStore } from '../../core/store/channels.store';
@Component({
selector: 'app-clients',
templateUrl: './clients.component.html',
styleUrls: ['./clients.component.scss']
})
export class ClientsComponent implements OnInit {
clients: Observable<Client[]>;
displayedColumns = ['id', 'name', 'type', 'payload', 'actions'];
constructor(
private dialog: MatDialog,
public clientsStore: ClientsStore,
public channelsStore: ChannelsStore,
) { }
ngOnInit() {
this.clientsStore.getClients();
this.channelsStore.getChannels();
}
addClient() {
const dialogRef = this.dialog.open(AddClientDialogComponent);
dialogRef.componentInstance.submit.subscribe((client: Client) => {
this.clientsStore.addClient(client);
});
}
editClient(client: Client) {
const dialogRef = this.dialog.open(AddClientDialogComponent, {
data: client
});
dialogRef.componentInstance.submit.subscribe((editedClient: Client) => {
this.clientsStore.editClient(toJS(editedClient));
});
}
deleteClient(client: Client) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
question: 'Are you sure you want to delete the client?'
}
});
dialogRef.afterClosed().subscribe((result) => {
if (result) {
this.clientsStore.deleteClient(toJS(client));
}
});
}
}
@@ -1,9 +1,9 @@
<form [formGroup]="addClientForm" (ngSubmit)="onAddClient()">
<h1 mat-dialog-title>Add client</h1>
<form [formGroup]="addThingForm" (ngSubmit)="onAddThing()">
<h1 mat-dialog-title>Add thing</h1>
<div mat-dialog-content fxLayout="column">
<input type="hidden" formControlName="id">
<mat-form-field>
<mat-select placeholder="Client type" formControlName="type">
<mat-form-field *ngIf="!editMode">
<mat-select placeholder="Thing type" formControlName="type">
<mat-option value="app">
App
</mat-option>
@@ -11,14 +11,14 @@
Device
</mat-option>
</mat-select>
<mat-error *ngIf="addClientForm.get('type').errors?.required">
<mat-error *ngIf="addThingForm.get('type').errors?.required">
Type is required
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
<mat-error *ngIf="addClientForm.get('name').errors?.required">
<mat-error *ngIf="addThingForm.get('name').errors?.required">
Name is required
</mat-error>
</mat-form-field>
@@ -29,6 +29,6 @@
</div>
<div mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addClientForm.invalid">Ok</button>
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addThingForm.invalid">Ok</button>
</div>
</form>
</form>
@@ -9,16 +9,16 @@ import { MaterialModule } from '../../../core/material/material.module';
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../../core/services/channels/channels.service';
import { ClientsService } from '../../../core/services/clients/clients.service';
import { AddClientDialogComponent } from './add-client-dialog.component';
import { ThingsService } from '../../../core/services/things/things.service';
import { ThingDialogComponent } from './thing-dialog.component';
describe('AddClientDialogComponent', () => {
let component: AddClientDialogComponent;
let fixture: ComponentFixture<AddClientDialogComponent>;
describe('ThingDialogComponent', () => {
let component: ThingDialogComponent;
let fixture: ComponentFixture<ThingDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddClientDialogComponent ],
declarations: [ ThingDialogComponent ],
imports: [
MaterialModule,
MatDialogModule,
@@ -31,7 +31,7 @@ describe('AddClientDialogComponent', () => {
providers: [
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -41,7 +41,7 @@ describe('AddClientDialogComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddClientDialogComponent);
fixture = TestBed.createComponent(ThingDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,44 @@
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Thing } from '../../../core/store/models';
@Component({
selector: 'app-thing-dialog',
templateUrl: './thing-dialog.component.html',
styleUrls: ['./thing-dialog.component.scss']
})
export class ThingDialogComponent implements OnInit {
addThingForm: FormGroup;
@Output() submit: EventEmitter<Thing> = new EventEmitter<Thing>();
editMode: boolean;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<ThingDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: Thing
) { }
ngOnInit() {
this.addThingForm = this.fb.group(
{
id: null,
type: ['', [Validators.required]],
name: ['', [Validators.required, Validators.minLength(5)]],
payload: ['']
}
);
if (this.data) {
this.editMode = true;
this.addThingForm.patchValue(this.data);
}
}
onAddThing() {
const thing = this.addThingForm.value;
this.submit.emit(thing);
this.dialogRef.close();
}
}
@@ -1,9 +1,9 @@
<div class="clientsContainer" fxLayout="row" fxLayoutWrap>
<ng-container [ngSwitch]="clientsStore.clients?.length > 0">
<div class="thingsContainer" fxLayout="row" fxLayoutWrap>
<ng-container [ngSwitch]="thingsStore.things?.length > 0">
<ng-container *ngSwitchCase="true">
<!-- List -->
<div class="client-list-container">
<mat-table #table [dataSource]="clientsStore.clients.toJS()" matSort class="mat-cell">
<div class="things-list-container">
<mat-table #table [dataSource]="thingsStore.things.toJS()" matSort class="mat-cell">
<!-- Columns -->
<ng-container matColumnDef="id">
<mat-header-cell fxFlex="32%" *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
@@ -25,10 +25,10 @@
<ng-container matColumnDef="actions">
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell fxFlex="8%" *matCellDef="let row" >
<button mat-icon-button color="accent" (click)="editClient(row)">
<button mat-icon-button color="accent" (click)="editThing(row)">
<mat-icon aria-label="Delete">edit</mat-icon>
</button>
<button mat-icon-button color="accent" (click)="deleteClient(row)">
<button mat-icon-button color="accent" (click)="deleteThing(row)">
<mat-icon aria-label="Delete">delete</mat-icon>
</button>
</mat-cell>
@@ -46,6 +46,6 @@
</ng-container>
</div>
<button mat-mini-fab class="addButton" (click)="addClient()">
<button mat-mini-fab class="addButton" (click)="addThing()">
<mat-icon>add</mat-icon>
</button>
@@ -5,6 +5,6 @@
}
/* List container */
.client-list-container {
.things-list-container {
width: 100%;
}
@@ -9,19 +9,19 @@ import { MaterialModule } from '../../core/material/material.module';
import { AuthenticationService } from '../../core/services/auth/authentication.service';
import { TokenStorage } from '../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../core/services/channels/channels.service';
import { ClientsService } from '../../core/services/clients/clients.service';
import { ClientsComponent } from './clients.component';
import { ThingsService } from '../../core/services/things/things.service';
import { ThingsComponent } from './things.component';
import { UiStore } from '../../core/store/ui.store';
import { ClientsStore } from '../../core/store/clients.store';
import { ThingsStore } from '../../core/store/things.store';
import { ChannelsStore } from '../../core/store/channels.store';
describe('ClientsComponent', () => {
let component: ClientsComponent;
let fixture: ComponentFixture<ClientsComponent>;
describe('ThingsComponent', () => {
let component: ThingsComponent;
let fixture: ComponentFixture<ThingsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ClientsComponent ],
declarations: [ ThingsComponent ],
imports: [
MaterialModule,
MatDialogModule,
@@ -34,15 +34,15 @@ describe('ClientsComponent', () => {
providers: [
UiStore,
{
provide: ClientsStore,
provide: ThingsStore,
useClass: class {
getClients = jasmine.createSpy('getClients');
getThings = jasmine.createSpy('getThings');
}
},
ChannelsStore,
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -52,7 +52,7 @@ describe('ClientsComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClientsComponent);
fixture = TestBed.createComponent(ThingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,63 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { toJS } from 'mobx';
import { Observable } from 'rxjs/Observable';
import { Thing } from '../../core/store/models';
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
import { ThingDialogComponent } from './thing-dialog/thing-dialog.component';
import { ThingsStore } from '../../core/store/things.store';
import { ChannelsStore } from '../../core/store/channels.store';
@Component({
selector: 'app-things',
templateUrl: './things.component.html',
styleUrls: ['./things.component.scss']
})
export class ThingsComponent implements OnInit {
things: Observable<Thing[]>;
displayedColumns = ['id', 'name', 'type', 'payload', 'actions'];
constructor(
private dialog: MatDialog,
public thingsStore: ThingsStore,
public channelsStore: ChannelsStore,
) { }
ngOnInit() {
this.thingsStore.getThings();
this.channelsStore.getChannels();
}
addThing() {
const dialogRef = this.dialog.open(ThingDialogComponent);
dialogRef.componentInstance.submit.subscribe((thing: Thing) => {
this.thingsStore.addThing(thing);
});
}
editThing(thing: Thing) {
const dialogRef = this.dialog.open(ThingDialogComponent, {
data: thing
});
dialogRef.componentInstance.submit.subscribe((editedThing: Thing) => {
this.thingsStore.editThing(toJS(editedThing));
});
}
deleteThing(thing: Thing) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
question: 'Are you sure you want to delete the thing?'
}
});
dialogRef.afterClosed().subscribe((result) => {
if (result) {
this.thingsStore.deleteThing(toJS(thing));
}
});
}
}
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/Observable';
import { forkJoin } from 'rxjs/observable/forkJoin';
import { environment } from '../../../../environments/environment';
import { Channel, Client } from '../../store/models';
import { Channel, Thing } from '../../store/models';
interface ChannelsPayload {
channels: Channel[];
@@ -36,10 +36,14 @@ export class ChannelsService {
name: channel.name
};
if (!channel.connected.length) {
console.log('send add ch');
return this.http.post(environment.channelsUrl, payload);
}
return this.http.post(environment.channelsUrl, payload, { observe: 'response' })
.switchMap((res) => {
const id = this.getChannelIdFrom(res);
return forkJoin(this.createClientConnectRequests(id, channel.connected));
return forkJoin(this.createThingsConnectRequests(id, channel.connected));
});
}
@@ -60,44 +64,44 @@ export class ChannelsService {
const editChannel = this.http.put(environment.channelsUrl + '/' + channel.id, payload);
return editChannel.switchMap(() => {
const clientsToAdd = this.getClientsToAdd(channelFormData, channel);
if (clientsToAdd.length) {
return forkJoin(this.createClientConnectRequests(channel.id, clientsToAdd));
const thingsToAdd = this.getThingsToAdd(channelFormData, channel);
if (thingsToAdd.length) {
return forkJoin(this.createThingsConnectRequests(channel.id, thingsToAdd));
} else {
return Observable.of([]);
}
}).switchMap(() => {
const clientsToDelete = this.getClientsToDelete(channelFormData, channel);
console.log(clientsToDelete);
if (clientsToDelete.length) {
return forkJoin(this.createClientDisconnectRequests(channel.id, clientsToDelete));
const thingsToDelete = this.getThingsToDelete(channelFormData, channel);
console.log(thingsToDelete);
if (thingsToDelete.length) {
return forkJoin(this.createThingsDisconnectRequests(channel.id, thingsToDelete));
} else {
return Observable.of([]);
}
});
}
getClientsToDelete(channelFormData: Channel, channel: Channel) {
return channel.connected.filter(client => {
return channelFormData.connected.find(cl => cl.id === client.id) === undefined;
getThingsToDelete(channelFormData: Channel, channel: Channel) {
return channel.connected.filter(thing => {
return channelFormData.connected.find(th => th.id === thing.id) === undefined;
});
}
getClientsToAdd(channelFormData: Channel, channel: Channel) {
return channelFormData.connected.filter(client => {
return channel.connected.find(cl => cl.id === client.id) === undefined;
getThingsToAdd(channelFormData: Channel, channel: Channel) {
return channelFormData.connected.filter(thing => {
return channel.connected.find(th => th.id === thing.id) === undefined;
});
}
createClientConnectRequests(channelId: string , connected: Client[]) {
createThingsConnectRequests(channelId: string , connected: Thing[]) {
return connected.map((connection) => {
return this.http.put(environment.channelsUrl + '/' + channelId + '/clients/' + connection.id, {});
return this.http.put(environment.channelsUrl + '/' + channelId + '/things/' + connection.id, {});
});
}
createClientDisconnectRequests(channelId: string , connected: Client[]) {
createThingsDisconnectRequests(channelId: string , connected: Thing[]) {
return connected.map((connection) => {
return this.http.delete(environment.channelsUrl + '/' + channelId + '/clients/' + connection.id, {});
return this.http.delete(environment.channelsUrl + '/' + channelId + '/things/' + connection.id, {});
});
}
}
@@ -1,27 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { Client } from '../../store/models';
@Injectable()
export class ClientsService {
constructor(private http: HttpClient) { }
getClients() {
return this.http.get(environment.clientsUrl);
}
addClient(client: Client) {
return this.http.post(environment.clientsUrl, client);
}
deleteClient(client: Client) {
return this.http.delete(environment.clientsUrl + '/' + client.id);
}
editClient(client: Client) {
return this.http.put(environment.clientsUrl + '/' + client.id, client);
}
}
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Client } from '../store/models';
import { Thing } from '../store/models';
const MOCK_CHANNELS = {
channels: [
@@ -20,7 +20,7 @@ export class MockChannelsService {
return Observable.of(MOCK_CHANNELS).delay(1000);
}
addChannel(client: Client) {
addChannel(client: Thing) {
MOCK_CHANNELS.channels.push(client);
return Observable.of(1).delay(1000);
}
@@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Client } from '../store/models';
let MOCK_CLIENTS = [
];
@Injectable()
export class MockClientsService {
getClients() {
return Observable.of(MOCK_CLIENTS).delay(1000);
}
addClient(client: Client) {
MOCK_CLIENTS.push(client);
return Observable.of(1).delay(1000);
}
}
@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Thing } from '../store/models';
const MOCK_THINGS = [
];
@Injectable()
export class MockThingsService {
getThings() {
return Observable.of(MOCK_THINGS).delay(1000);
}
addThing(thing: Thing) {
MOCK_THINGS.push(thing);
return Observable.of(1).delay(1000);
}
}
@@ -0,0 +1,27 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { Thing } from '../../store/models';
@Injectable()
export class ThingsService {
constructor(private http: HttpClient) { }
getThings() {
return this.http.get(environment.thingsUrl);
}
addThing(thing: Thing) {
return this.http.post(environment.thingsUrl, thing);
}
deleteThing(thing: Thing) {
return this.http.delete(environment.thingsUrl + '/' + thing.id);
}
editThing(thing: Thing) {
return this.http.put(environment.thingsUrl + '/' + thing.id, thing);
}
}
@@ -7,7 +7,7 @@ import { Observable } from 'rxjs/Observable';
import { AuthenticationService } from '../services/auth/authentication.service';
import { TokenStorage } from '../services/auth/token-storage.service';
import { ChannelsService } from '../services/channels/channels.service';
import { ClientsService } from '../services/clients/clients.service';
import { ThingsService } from '../services/things/things.service';
import { AuthStore } from './auth.store';
import { UiStore } from './ui.store';
@@ -23,7 +23,7 @@ describe('AuthStore', () => {
UiStore,
TokenStorage,
AuthenticationService,
ClientsService,
ThingsService,
ChannelsService,
]
});
@@ -59,14 +59,14 @@ describe('AuthStore', () => {
}));
it('should navigate to /clients when successfully authenticated', inject([AuthStore, AuthenticationService, Router],
it('should navigate to /things when successfully authenticated', inject([AuthStore, AuthenticationService, Router],
(authStore: AuthStore, authService: AuthenticationService, router: Router) => {
const spy = spyOn(authService, 'login').and.returnValue(Observable.of(true));
const routerSpy = spyOn(router, 'navigate').and.stub();
authStore.login(user);
expect(routerSpy).toHaveBeenCalledWith(['/clients']);
expect(routerSpy).toHaveBeenCalledWith(['/things']);
}));
it('should set the loading flag to false when successfully authenticated',
+1 -1
View File
@@ -31,7 +31,7 @@ export class AuthStore {
.subscribe(() => {
this.uiState.loading = false;
this.isAuthenticated = true;
this.router.navigate(['/clients']);
this.router.navigate(['/things']);
},
(error) => {
this.uiState.loading = false;
@@ -29,8 +29,10 @@ export class ChannelsStore {
@action
addChannel(channel: Channel) {
this.uiState.loading = true;
console.log('add ch');
this.channelsService.addChannel(channel)
.subscribe(() => {
.subscribe(resp => {
console.log(resp);
this.uiState.loading = false;
this.getChannels();
}, () => {
@@ -1,260 +0,0 @@
import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { toJS } from 'mobx';
import { Observable } from 'rxjs/Observable';
import { ClientsService } from '../services/clients/clients.service';
import { ClientsStore } from './clients.store';
import { Client } from './models';
import { UiStore } from './ui.store';
describe('ClientsStore', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
RouterTestingModule.withRoutes([])
],
providers: [
ClientsStore,
UiStore,
ClientsService,
]
});
});
it('should be created', inject([ClientsStore], (clientsStore: ClientsStore) => {
expect(clientsStore).toBeTruthy();
}));
describe('getClients', () => {
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const getClients = spyOn(clientsService, 'getClients').and.returnValue({ subscribe: () => { } });
clientsStore.getClients();
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful get', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const getClients = spyOn(clientsService, 'getClients').and.returnValue(Observable.of(true));
clientsStore.getClients();
expect(uiStore.loading).toBeFalsy();
}));
it('should set the clients property to the returned clients from the service', inject([ClientsStore, ClientsService],
(clientsStore: ClientsStore, clientsService: ClientsService) => {
const serviceReturnValue = { clients: [] };
const getChannels = spyOn(clientsService, 'getClients').and.returnValue(Observable.of(serviceReturnValue));
clientsStore.getClients();
expect(toJS(clientsStore.clients)).toEqual(serviceReturnValue.clients);
}));
it('should set the loading flag to false after failed get', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const getClients = spyOn(clientsService, 'getClients').and.returnValue(Observable.throw(''));
clientsStore.getClients();
expect(uiStore.loading).toBeFalsy();
}));
});
describe('addClient', () => {
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const addClient = spyOn(clientsService, 'addClient').and.returnValue({ subscribe: () => { } });
const newClient: Client = {
name: 'new client',
type: 'app',
payload: '',
};
clientsStore.addClient(newClient);
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.of(true));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const newClient: Client = {
name: 'new client',
type: 'app',
payload: '',
};
clientsStore.addClient(newClient);
expect(uiStore.loading).toBeFalsy();
}));
it('should call the ClientsStore.getClients after successful add', inject([ClientsStore, ClientsService],
(clientsStore: ClientsStore, clientsService: ClientsService) => {
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.of(true));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const newClient: Client = {
name: 'new client',
type: 'app',
payload: '',
};
clientsStore.addClient(newClient);
expect(storeGetClientsSpy).toHaveBeenCalled();
}));
it('should set the loading flag to false after failed add', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const addClient = spyOn(clientsService, 'addClient').and.returnValue(Observable.throw(''));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const newClient: Client = {
name: 'new client',
type: 'app',
payload: '',
};
clientsStore.addClient(newClient);
expect(uiStore.loading).toBeFalsy();
}));
});
describe('editClient', () => {
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const editClient = spyOn(clientsService, 'editClient').and.returnValue({ subscribe: () => { } });
const editedClient: Client = {
name: 'edited client',
type: 'app',
payload: '',
};
clientsStore.editClient(editedClient);
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.of(true));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const editedClient: Client = {
name: 'edited client',
type: 'app',
payload: '',
};
clientsStore.editClient(editedClient);
expect(uiStore.loading).toBeFalsy();
}));
it('should call the ClientsStore.getChannels after successful edit', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.of(true));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const editedClient: Client = {
name: 'edited client',
type: 'app',
payload: '',
};
clientsStore.editClient(editedClient);
expect(storeGetClientsSpy).toHaveBeenCalled();
}));
it('should set the loading flag to false after failed edit', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const editClient = spyOn(clientsService, 'editClient').and.returnValue(Observable.throw(''));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const editedClient: Client = {
name: 'edited client',
type: 'app',
payload: '',
};
clientsStore.editClient(editedClient);
expect(uiStore.loading).toBeFalsy();
}));
});
describe('deleteClient', () => {
it('should set the loading flag to true before service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue({ subscribe: () => { } });
const clientToBeDeleted: Client = {
name: 'clientToBeDeleted',
type: 'app',
payload: ''
};
clientsStore.deleteClient(clientToBeDeleted);
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful service call', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.of(true));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const clientToBeDeleted: Client = {
name: 'clientToBeDeleted',
type: 'app',
payload: ''
};
clientsStore.deleteClient(clientToBeDeleted);
expect(uiStore.loading).toBeFalsy();
}));
it('should call the clientsStore.getChannels after successful add', inject([ClientsStore, ClientsService],
(clientsStore: ClientsStore, clientsService: ClientsService) => {
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.of(true));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const clientToBeDeleted: Client = {
name: 'clientToBeDeleted',
type: 'app',
payload: ''
};
clientsStore.deleteClient(clientToBeDeleted);
expect(storeGetClientsSpy).toHaveBeenCalled();
}));
it('should set the loading flag to false after failed delete', inject([ClientsStore, UiStore, ClientsService],
(clientsStore: ClientsStore, uiStore: UiStore, clientsService: ClientsService) => {
const deleteClient = spyOn(clientsService, 'deleteClient').and.returnValue(Observable.throw(''));
const storeGetClientsSpy = spyOn(clientsStore, 'getClients').and.stub();
const clientToBeDeleted: Client = {
name: 'clientToBeDeleted',
type: 'app',
payload: ''
};
clientsStore.deleteClient(clientToBeDeleted);
expect(uiStore.loading).toBeFalsy();
}));
});
});
+2 -2
View File
@@ -1,10 +1,10 @@
export interface Channel {
id?: '';
name: string;
connected: Client[];
connected: Thing[];
}
export interface Client {
export interface Thing {
id?: '';
type: string;
name: string;
@@ -0,0 +1,256 @@
import { HttpClientModule } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { toJS } from 'mobx';
import { Observable } from 'rxjs/Observable';
import { ThingsService } from '../services/things/things.service';
import { ThingsStore } from './things.store';
import { Thing } from './models';
import { UiStore } from './ui.store';
describe('ThingsStore', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
RouterTestingModule.withRoutes([])
],
providers: [
ThingsStore,
UiStore,
ThingsService,
]
});
});
it('should be created', inject([ThingsStore], (thingsStore: ThingsStore) => {
expect(thingsStore).toBeTruthy();
}));
describe('getThings', () => {
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const getThings = spyOn(thingsService, 'getThings').and.returnValue({ subscribe: () => { } });
thingsStore.getThings();
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful get', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const getThings = spyOn(thingsService, 'getThings').and.returnValue(Observable.of(true));
thingsStore.getThings();
expect(uiStore.loading).toBeFalsy();
}));
it('should set the things property to the returned things from the service', inject([ThingsStore, ThingsService],
(thingsStore: ThingsStore, thingsService: ThingsService) => {
const serviceReturnValue = { things: [] };
const getChannels = spyOn(thingsService, 'getThings').and.returnValue(Observable.of(serviceReturnValue));
thingsStore.getThings();
expect(toJS(thingsStore.things)).toEqual(serviceReturnValue.things);
}));
it('should set the loading flag to false after failed get', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const getThings = spyOn(thingsService, 'getThings').and.returnValue(Observable.throw(''));
thingsStore.getThings();
expect(uiStore.loading).toBeFalsy();
}));
});
describe('addThing', () => {
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const addThing = spyOn(thingsService, 'addThing').and.returnValue({ subscribe: () => { } });
const newThing: Thing = {
name: 'new thing',
type: 'app',
payload: '',
};
thingsStore.addThing(newThing);
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.of(true));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const newThing: Thing = {
name: 'new thing',
type: 'app',
payload: '',
};
thingsStore.addThing(newThing);
expect(uiStore.loading).toBeFalsy();
}));
it('should call the thingsStore.getThings after successful add', inject([ThingsStore, ThingsService],
(thingsStore: ThingsStore, thingsService: ThingsService) => {
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.of(true));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const newThing: Thing = {
name: 'new thing',
type: 'app',
payload: '',
};
thingsStore.addThing(newThing);
expect(storeGetThingsSpy).toHaveBeenCalled();
}));
it('should set the loading flag to false after failed add', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const addThing = spyOn(thingsService, 'addThing').and.returnValue(Observable.throw(''));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const newThing: Thing = {
name: 'new thing',
type: 'app',
payload: '',
};
thingsStore.addThing(newThing);
expect(uiStore.loading).toBeFalsy();
}));
});
describe('editThing', () => {
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const editThing = spyOn(thingsService, 'editThing').and.returnValue({ subscribe: () => { } });
const editedThing: Thing = {
name: 'edited thing',
type: 'app',
payload: '',
};
thingsStore.editThing(editedThing);
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.of(true));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const editedThing: Thing = {
name: 'edited thing',
type: 'app',
payload: '',
};
thingsStore.editThing(editedThing);
expect(uiStore.loading).toBeFalsy();
}));
it('should call the thingsStore.getChannels after successful edit', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.of(true));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const editedThing: Thing = {
name: 'edited thing',
type: 'app',
payload: '',
};
thingsStore.editThing(editedThing);
expect(storeGetThingsSpy).toHaveBeenCalled();
}));
it('should set the loading flag to false after failed edit', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const editThing = spyOn(thingsService, 'editThing').and.returnValue(Observable.throw(''));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const editedThing: Thing = {
name: 'edited thing',
type: 'app',
payload: '',
};
thingsStore.editThing(editedThing);
expect(uiStore.loading).toBeFalsy();
}));
});
describe('deleteThing', () => {
it('should set the loading flag to true before service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue({ subscribe: () => { } });
const thingToBeDeleted: Thing = {
name: 'thingToBeDeleted',
type: 'app',
payload: ''
};
thingsStore.deleteThing(thingToBeDeleted);
expect(uiStore.loading).toBeTruthy();
}));
it('should set the loading flag to false after successful service call', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.of(true));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const thingToBeDeleted: Thing = {
name: 'thingToBeDeleted',
type: 'app',
payload: ''
};
thingsStore.deleteThing(thingToBeDeleted);
expect(uiStore.loading).toBeFalsy();
}));
it('should call the thingsStore.getChannels after successful add', inject([ThingsStore, ThingsService],
(thingsStore: ThingsStore, thingsService: ThingsService) => {
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.of(true));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const thingToBeDeleted: Thing = {
name: 'thingToBeDeleted',
type: 'app',
payload: ''
};
thingsStore.deleteThing(thingToBeDeleted);
expect(storeGetThingsSpy).toHaveBeenCalled();
}));
it('should set the loading flag to false after failed delete', inject([ThingsStore, UiStore, ThingsService],
(thingsStore: ThingsStore, uiStore: UiStore, thingsService: ThingsService) => {
const deleteThing = spyOn(thingsService, 'deleteThing').and.returnValue(Observable.throw(''));
const storeGetThingsSpy = spyOn(thingsStore, 'getThings').and.stub();
const thingToBeDeleted: Thing = {
name: 'thingToBeDeleted',
type: 'app',
payload: ''
};
thingsStore.deleteThing(thingToBeDeleted);
expect(uiStore.loading).toBeFalsy();
}));
});
});
@@ -1,62 +1,62 @@
import { Injectable } from '@angular/core';
import { action, observable } from 'mobx';
import { ClientsService } from '../services/clients/clients.service';
import { Client } from './models';
import { ThingsService } from '../services/things/things.service';
import { Thing } from './models';
import { UiStore } from './ui.store';
@Injectable()
export class ClientsStore {
@observable clients: Client[] = [];
export class ThingsStore {
@observable things: Thing[] = [];
constructor(
private uiState: UiStore,
private clientsService: ClientsService,
private thingsService: ThingsService,
) { }
@action
getClients() {
getThings() {
this.uiState.loading = true;
this.clientsService.getClients()
this.thingsService.getThings()
.subscribe((payload: any) => {
this.uiState.loading = false;
this.clients = payload.clients;
this.things = payload.things;
}, () => {
this.uiState.loading = false;
});
}
@action
addClient(client: Client) {
addThing(thing: Thing) {
this.uiState.loading = true;
this.clientsService.addClient(client)
this.thingsService.addThing(thing)
.subscribe(() => {
this.uiState.loading = false;
this.getClients();
this.getThings();
}, () => {
this.uiState.loading = false;
});
}
@action
editClient(client: Client) {
editThing(thing: Thing) {
this.uiState.loading = true;
this.clientsService.editClient(client)
this.thingsService.editThing(thing)
.subscribe(() => {
this.uiState.loading = false;
this.getClients();
this.getThings();
}, () => {
this.uiState.loading = false;
});
}
@action
deleteClient(client: Client) {
deleteThing(thing: Thing) {
this.uiState.loading = true;
this.clientsService.deleteClient(client)
this.thingsService.deleteThing(thing)
.subscribe(() => {
this.uiState.loading = false;
this.getClients();
this.getThings();
}, () => {
this.uiState.loading = false;
});
+2 -2
View File
@@ -6,7 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { AuthenticationService } from '../services/auth/authentication.service';
import { TokenStorage } from '../services/auth/token-storage.service';
import { ChannelsService } from '../services/channels/channels.service';
import { ClientsService } from '../services/clients/clients.service';
import { ThingsService } from '../services/things/things.service';
import { UiStore } from './ui.store';
describe('State', () => {
@@ -21,7 +21,7 @@ describe('State', () => {
UiStore,
TokenStorage,
AuthenticationService,
ClientsService,
ThingsService,
ChannelsService,
]
});
@@ -2,6 +2,6 @@ export const environment = {
production: true,
signupUrl: '/users',
loginUrl: '/tokens',
clientsUrl: '/clients',
thingsUrl: '/things',
channelsUrl: '/channels'
};
+1 -1
View File
@@ -7,6 +7,6 @@ export const environment = {
production: false,
signupUrl: '/users',
loginUrl: '/tokens',
clientsUrl: '/clients',
thingsUrl: '/things',
channelsUrl: '/channels'
};
+49
View File
@@ -0,0 +1,49 @@
###
# This docker-compose file contains optional InfluxDB, InfluxDB-writer and Grafana services
# for Mainflux platform. Since these are optional, this file is dependent of docker-compose file
# from <project_root>/docker. In order to run these optional service, execute command:
# docker-compose -f docker/docker-compose.yml -f docker/addons/influxdb/docker-compose.yml up
# from project root.
###
version: "3"
services:
influxdb:
image: influxdb:1.5.2-alpine
container_name: mainflux-influxdb
restart: on-failure
environment:
INFLUXDB_DB: mainflux
INFLUXDB_ADMIN_USER: mainflux
INFLUXDB_ADMIN_PASSWORD: mainflux
influxdb-writer:
image: mainflux/influxdb:latest
container_name: mainflux-influxdb-writer
depends_on:
- influxdb
- nats
expose:
- 8900
restart: on-failure
environment:
MF_NATS_URL: nats://nats:4222
MF_INFLUX_WRITER_PORT: 8900
MF_INFLUX_WRITER_DB_NAME: mainflux
MF_INFLUX_WRITER_DB_HOST: influxdb
MF_INFLUX_WRITER_DB_PORT: 8086
MF_INFLUX_WRITER_DB_USER: mainflux
MF_INFLUX_WRITER_DB_PASS: mainflux
ports:
- 8900:8900
grafana:
image: grafana/grafana:5.1.3
container_name: mainflux-grafana
depends_on:
- influxdb
restart: on-failure
ports:
- 3001:3000
+41
View File
@@ -0,0 +1,41 @@
###
# This docker-compose file contains optional MongoDB and MongoDB-writer services
# for Mainflux platform. Since these are optional, this file is dependent of docker-compose file
# from <project_root>/docker. In order to run these optional service, execute command:
# docker-compose -f docker/docker-compose.yml -f docker/addons/mongodb/docker-compose.yml up
# from project root. MongoDB default port (27017) is exposed, so you can use various tools for database
# inspection and data visualization.
###
version: "3"
services:
mongodb:
image: mongo:3.6-jessie
container_name: mainflux-mongodb
expose:
- 27017
restart: on-failure
environment:
MONGO_INITDB_DATABASE: mainflux
ports:
- 27017:27017
mongo-writer:
image: mainflux/mongodb:latest
container_name: mainflux-mongodb-writer
depends_on:
- mongodb
- nats
expose:
- 8901
restart: on-failure
environment:
MF_NATS_URL: nats://nats:4222
MF_MONGO_WRITER_PORT: 8901
MF_MONGO_WRITER_DB_NAME: mainflux
MF_MONGO_WRITER_DB_HOST: mongodb
MF_MONGO_WRITER_DB_PORT: 27017
ports:
- 8901:8901
+51 -44
View File
@@ -14,14 +14,14 @@ services:
container_name: mainflux-nginx
restart: on-failure
volumes:
- $PWD/nginx.conf:/etc/nginx/nginx.conf
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- ./ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- ./ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
ports:
- "80:80"
- "443:443"
- "8883:8883"
- 80:80
- 443:443
- 8883:8883
nats:
image: nats:1.0.2
@@ -58,35 +58,35 @@ services:
ports:
- 8180:8180
clients-db:
things-db:
image: postgres:10.2-alpine
container_name: mainflux-clients-db
container_name: mainflux-things-db
restart: on-failure
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: clients
POSTGRES_DB: things
clients:
image: mainflux/clients:latest
container_name: mainflux-clients
things:
image: mainflux/things:latest
container_name: mainflux-things
depends_on:
- clients-db
- things-db
- users
expose:
- 8182
- 8183
restart: on-failure
environment:
MF_CLIENTS_DB_HOST: clients-db
MF_CLIENTS_DB_PORT: 5432
MF_CLIENTS_DB_USER: mainflux
MF_CLIENTS_DB_PASS: mainflux
MF_CLIENTS_DB: clients
MF_CLIENTS_HTTP_PORT: 8182
MF_CLIENTS_GRPC_PORT: 8183
MF_THINGS_DB_HOST: things-db
MF_THINGS_DB_PORT: 5432
MF_THINGS_DB_USER: mainflux
MF_THINGS_DB_PASS: mainflux
MF_THINGS_DB: things
MF_THINGS_HTTP_PORT: 8182
MF_THINGS_GRPC_PORT: 8183
MF_USERS_URL: users:8181
MF_CLIENTS_SECRET: secret
MF_THINGS_SECRET: secret
ports:
- 8182:8182
@@ -95,52 +95,59 @@ services:
container_name: mainflux-normalizer
restart: on-failure
expose:
- 8181
- 8184
environment:
MF_NATS_URL: nats://nats:4222
MF_NORMALIZER_PORT: 8181
MF_NORMALIZER_PORT: 8184
coap:
image: mainflux/coap:latest
container_name: mainflux-coap
depends_on:
- clients
expose:
- 5683
dashflux:
image: mainflux/dashflux:latest
container_name: mainflux-dashflux
restart: on-failure
environment:
MF_COAP_ADAPTER_PORT: 5683
MF_NATS_URL: nats://nats:4222
MF_CLIENTS_URL: clients:8183
ports:
- 5683:5683/udp
- 3000:3000
ws:
ws-adapter:
image: mainflux/ws:latest
container_name: mainflux-ws
depends_on:
- clients
- things
expose:
- 8184
- 8186
restart: on-failure
environment:
MF_WS_ADAPTER_PORT: 8184
MF_WS_ADAPTER_PORT: 8186
MF_NATS_URL: nats://nats:4222
MF_CLIENTS_URL: clients:8183
MF_THINGS_URL: things:8183
ports:
- 8184:8184
- 8186:8186
http-adapter:
image: mainflux/http:latest
container_name: mainflux-http
depends_on:
- clients
- things
restart: on-failure
expose:
- 8185
environment:
MF_HTTP_ADAPTER_PORT: 8185
MF_NATS_URL: nats://nats:4222
MF_CLIENTS_URL: clients:8183
MF_THINGS_URL: things:8183
ports:
- 8185:8185
mqtt-adapter:
image: mainflux/mqtt:latest
container_name: mainflux-mqtt
depends_on:
- things
restart: on-failure
environment:
MF_MQTT_ADAPTER_PORT: 1883
MF_MQTT_WS_PORT: 8880
MF_NATS_URL: nats://nats:4222
MF_THINGS_URL: things:8183
ports:
- 1883:1883
- 8880:8880
+148 -43
View File
@@ -116,59 +116,164 @@ http {
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers "*";
server_name localhost;
# Proxy pass to users service
location ~ ^/(users|tokens) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://users:8180;
# Proxy pass to users service
location ~ ^/(users|tokens) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://users:8180;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
# Proxy pass to clients service
location ~ ^/(clients|channels) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://clients:8182;
# Proxy pass to things service
location ~ ^/(things|channels) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Access-Control-Expose-Headers Location;
proxy_pass http://things:8182;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
# Proxy pass to mainflux-http-adapter
location /http/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://http-adapter:8185/;
location /version {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://things:8182;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location /http/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://http-adapter:8185/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-ws-adapter
location /ws/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
proxy_pass http://ws-adapter:8186/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-mqtt-adapter
location /mqtt {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
proxy_pass http://mqtt-adapter:8880/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://dashflux:3000/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
}
# MQTT
stream {
# MQTT
server {
listen 8883 ssl;
listen [::]:8883 ssl;
# Certificates
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
proxy_pass mqtt-adapter:1883;
}
}
+11 -15
View File
@@ -5,7 +5,7 @@ Mainflux IoT platform is comprised of the following services:
| Service | Description |
|:--------------------------------------------------------------------------|:------------------------------------------------------------------------|
| [users](https://github.com/mainflux/mainflux/tree/master/users) | Manages platform's users and auth concerns |
| [clients](https://github.com/mainflux/mainflux/tree/master/clients) | Manages platform's clients, channels and access policies |
| [things](https://github.com/mainflux/mainflux/tree/master/things) | Manages platform's things, channels and access policies |
| [normalizer](https://github.com/mainflux/mainflux/tree/master/normalizer) | Normalizes SenML messages and generates the "processed" messages stream |
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
| [ws-adapter](https://github.com/mainflux/mainflux/tree/master/ws) | Provides a WebSocket interface for accessing communication channels |
@@ -14,26 +14,22 @@ Mainflux IoT platform is comprised of the following services:
## Domain model
The platform is built around 3 main entities: **users**, **clients** and **channels**.
The platform is built around 3 main entities: **users**, **things** and **channels**.
`User` represents the real (human) user of the system. It is represented via its
e-mail and password, which he uses as platform access credentials in order to obtain
an access token. Once logged into the system, user can manage his resources in
CRUD fashion (i.e. channels and clients), and define access control policies
between them.
an access token. Once logged into the system, user can manage his resources (i.e.
things and channels) in CRUD fashion, and define access control policies by
connecting them.
`Device` is used to represent any device that connects to Mainflux. It is a
generic model that describes any client device of the system.
`Application` is very similar to the `Device` and is represented by the same
`Client` structure (just with different `type` info). Application represents
an end-user application that communicates with devices through Mainflux, and
can be running somewhere in the cloud, locally on the PC or on the mobile phone.
Usually it acquires data from sensor measurement and displays it on various
dashboards.
`Thing` represents devices and applications connected to Mainflux. There are
two types of "things" supported at the moment: `device` and `app`. While device
is used to represent any physical device connected to the platform, app represents
any 3rd party service that uses the platform for message exchange with other
"things".
`Channel` represents a communication channel. It serves as message topic that
can be consumed by all of the clients connected to it.
can be consumed by all of the things connected to it.
## Messaging
+43 -47
View File
@@ -48,56 +48,56 @@ an authorization key.
### Provisioning devices
Devices are provisioned by executing request `POST /clients`, with a
Devices are provisioned by executing request `POST /things`, with a
`"type":"device"` specified in JSON payload. Note that you will also need
`user_auth_token` in order to provision clients (both devices and application)
`user_auth_token` in order to provision things (both devices and application)
that belong to this particular user.
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"device", "name":"weio"}'
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/things -d '{"type":"device", "name":"weio"}'
```
Response will contain `Location` header whose value represents path to newly
created client:
created thing:
```
HTTP/1.1 201 Created
Content-Type: application/json
Location: /clients/81380742-7116-4f6f-9800-14fe464f6773
Location: /things/81380742-7116-4f6f-9800-14fe464f6773
Date: Tue, 10 Apr 2018 10:02:59 GMT
Content-Length: 0
```
### Provisioning applications
Applications are provisioned by executing HTTP request `POST /clients`, with
Applications are provisioned by executing HTTP request `POST /things`, with
`"type":"app"` specified in JSON payload.
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"app", "name":"myapp"}'
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/things -d '{"type":"app", "name":"myapp"}'
```
Response will contain `Location` header whose value represents path to newly
created client (same as for devices):
created thing (same as for devices):
```
HTTP/1.1 201 Created
Content-Type: application/json
Location: /clients/cb63f852-2d48-44f0-a0cf-e450496c6c92
Location: /things/cb63f852-2d48-44f0-a0cf-e450496c6c92
Date: Tue, 10 Apr 2018 10:33:17 GMT
Content-Length: 0
```
### Retrieving provisioned clients
### Retrieving provisioned things
In order to retrieve data of provisioned clients that is written in database, you
In order to retrieve data of provisioned things that is written in database, you
can send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/things
```
Notice that you will receive only those clients that were provisioned by
Notice that you will receive only those things that were provisioned by
`user_auth_token` owner.
```
@@ -107,7 +107,7 @@ Date: Tue, 10 Apr 2018 10:50:12 GMT
Content-Length: 1105
{
"clients": [
"things": [
{
"id": "81380742-7116-4f6f-9800-14fe464f6773",
"type": "device",
@@ -125,22 +125,22 @@ Content-Length: 1105
```
You can specify `offset` and `limit` parameters in order to fetch specific
group of clients. In that case, your request should look like:
group of things. In that case, your request should look like:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients?offset=0&limit=5
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/things?offset=0&limit=5
```
If you don't provide them, default values will be used instead: 0 for `offset`,
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
invalid values will be considered malformed request.
### Removing clients
### Removing things
In order to remove you own client you can send following request:
In order to remove you own thing you can send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/clients/<client_id>
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/things/<thing_id>
```
### Provisioning channels
@@ -211,28 +211,28 @@ curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE
## Access control
Channel can be observed as a communication group of clients. Only clients that
are connected to the channel can send and receive messages from other clients
in this channel. Clients that are not connected to this channel are not allowed
Channel can be observed as a communication group of things. Only things that
are connected to the channel can send and receive messages from other things
in this channel. things that are not connected to this channel are not allowed
to communicate over it.
Only user, who is the owner of a channel and of the clients, can connect the
clients to the channel (which is equivalent of giving permissions to these clients
Only user, who is the owner of a channel and of the things, can connect the
things to the channel (which is equivalent of giving permissions to these things
to communicate over given communication group).
To connect client to the channel you should send following request:
To connect thing to the channel you should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/things/<thing_id>
```
You can observe which clients are connected to specific channel:
You can observe which things are connected to specific channel:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
```
You should receive response with the lists of connected clients in `connected` field
You should receive response with the lists of connected things in `connected` field
similar to this one:
```
@@ -253,21 +253,21 @@ similar to this one:
If you want to disconnect your device from the channel, send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/things/<thing_id>
```
## Sending messages
Once a channel is provisioned and client is connected to it, it can start to
Once a channel is provisioned and thing is connected to it, it can start to
publish messages on the channel. The following sections will provide an example
of message publishing for each of the supported protocols.
### HTTP
To publish message over channel, client should send following request:
To publish message over channel, thing should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <client_token>" https://localhost/http/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <thing_token>" https://localhost/http/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
```
Note that you should always send array of messages in senML format.
@@ -276,32 +276,28 @@ Note that you should always send array of messages in senML format.
To publish and receive messages over channel using web socket, you should first
send handshake request to `/channels/<channel_id>/messages` path. Don't forget
to send `Authorization` header with client authorization token.
to send `Authorization` header with thing authorization token.
If you are not able to send custom headers in your handshake request, send it as
query parameter `authorization`. Then your path should look like this
`/channels/<channel_id>/messages?authorization=<client_auth_key>`.
`/channels/<channel_id>/messages?authorization=<thing_auth_key>`.
### CoAP
### MQTT
These are two useful tools for sending CoAP messages:
To send and receive messages over MQTT you could use [Mosquitto tools](https://mosquitto.org),
or [Paho](https://www.eclipse.org/paho/) if you want to use MQTT over WebSocket.
- [Copper](https://github.com/mkovatsc/Copper)
- [coap-cli](https://github.com/mcollina/coap-cli)
To publish message over channel, client should send following confirmable message:
To publish message over channel, thing should call following command:
```
coap post coap://localhost/channels/<channel_id>/messages?key=<client_token>
mosquitto_pub -u <thing_id> -P <thing_key> -t channels/<channel_id>/messages -h localhost -m [{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]
```
Client token must be present in `Uri-Query` option. Otherwise, message will be considered unauthorized.
To subscribe to channel, client should send following confirmable message:
To subscribe to channel, thing should call following command:
```
coap get coap://localhost/channels/<channel_id>/messages?key=<client_token> -o
mosquitto_sub -u <thing_id> -P <thing_key> -t channels/<channel_id>/messages -h localhost
```
where `-o` in indicates that value of `Observe` option is 0.
To unsubscribe from channel, client should send same message with value of `Observe` option set to 1, ignore confirmable message received from service (by default, server will attempt to send confirmable message multiple times, as described in [RFC 7252](https://tools.ietf.org/html/rfc7252#page-28) or send `Reset` message with appropriate token.
If you are using TLS to secure MQTT connection, add `--cafile docker/ssl/certs/ca.crt`
to every command.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

+1 -1
View File
@@ -1 +1 @@
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0" version="8.6.1" editor="www.draw.io" type="device"><diagram id="82652b06-2c2b-a339-9c87-b7b8398097a5" name="Page-1">7Vpbc5s4FP41fmyGO/gxdtN0Z7IZ7yY7bZ8yMshYW4EYIcd2f/1KIGFAOGETfEmTZDKBgy7wne87OrqM7GmyuaYgW/5JIohHlhFtRvbnkWWZjm3wf8KyLS1jyywNMUWRLLQz3KFfUBplvXiFIpg3CjJCMENZ0xiSNIUha9gApWTdLLYguNlrBmKoGe5CgHXrNxSxZWkNXGNn/wpRvFQ9m4Z8Mgfhz5iSVSr7G1n2ovgpHydAtSXL50sQkXXNZF+N7CklhJVXyWYKscBWwVbW+7LnafXeFKasTwW/rPAI8Ep++u31H7ff5cuxrQLkEVKGOD43YA7xjOSIIZLyR3PCGElG9gTkWemIBdpA3vhkyRLMb01+qSpfYhSLSoxk3JozSn7CKcGEcltKUt7VhKwYRim3Kr8aouQSZOI9kk0s6HYRIkbR5iKjZLN9yCHl7fNS+qdLNET3cFMzSSiuIUkgo1teRD6t+Cdpa3lueb/ekcD3LqRxWWPAWFYEknhx1fYOfH4h8d/jPE9zxj/863LNGVg4gX/xeokYvMtAKOxrDk0TdpSULEdJzLsOMeLYMH51DQF9MK1gw/8usjTmRRcIY+UJTtkIwGARaj7iT7wwgPPFMGjb4ybappJXDW1TCaUOtjcA2LapgX3992yqYQ0jHhjkLaFsSWKSAny1s04KtQvKF1yt4Q83iH0XZk6Y8u6HLMQxo9vaI3H7Q9bKGffSpYhh3BBikOcoVOYvCKvG/4WMbWXgBCtGhHaq17shQmFFub1+ysmKhlDxXsZYQGMoi1nj0iYAeNKbFGLA0GMzcr7GNZbumhkljISEl/Iwf7/JnPKrWFyBCGTsHYnEMY4oEtc+jB4Cv6GISh8zSBF/SRHOy8DfTwswjfRC3Hgwudi6XPxTqcV1NLWM3EmmBON+1lzImceanqEwR7/AvCggcM8ISlnxTu5EtMBlJofukONSjLbtMT1BUVS4v9DdpEqDarKQiVAXyIpomi6qFE++XSNN6tLLJ+PCMA05QvcGWzY3E99dK0IWi5w7uO2Nqtd+4czWHDTFCAqANXFhHn9yWEt5QkxW0fNRrBWaoBm50O8KTWPPt4E3TGhqJ0t+z8gUDBCZOhJXnpWymMK7v240XCs0tzy5jAr+PgPovAxiN/M9CHN8g8jpQjiw5rY3EMKu14I40CEeHypBsj4SpF3EH+sRX6XrJ0iQxr0jym+aBXUJ4XBTBVcfYTUVdKUge5haV0DF8zrLS9L3Iqav81IVe5aXNezcDuiU7aWDqJpT260Etu2S8nNkrfpSRbuhoNWQ2WqoBEFr6AVDtq0L7O3lVBVtX51T8ZTKMoIG+J/M15Fj0AzL1hdOzlqgfWfWxxGo2RKo579QoGZLoJ51OIEGmse/3t/P7t6ELhVbB5nruPYr5zqbzgrD6FJP0ru9dEbi7J3VfYhzn9sdff3uGB5Xswk1s/ifXOhK8Xuv6nyQYS8Z9NWPdzBR79rKONRE3elYADx/uak4W5eb4sqZyM21mk612yrpKzfXf6ahAeWmz1VvL+/1EbdUg9rbtppub++3LjBZh0tOlosIUZ6fPESAgT3SbMlv4YrfzuWB4mcg+bluA2DH1Pdtqy36OlPsAfTn+brcBlgT67VBUgnU1AUagXxZ9KAtmsm99vqKmXGCbRQn0EOAd7JFNUefUrxD5bimPnAdTDlOxzJLr5GrQy/6MPTk4CVu22LaL5j9q8y9iN61va4C9ZmMdW8xtVSffnz6NJjSFZf7xb/zokA73XGcF1LAN1oNtWPFgBQIDjL2NvajGolwxzjbGUuOPN42CNmLfG7H2ocaAk9wiKHjHCKhCcD806nm4t9kU0vTyTGP9nj6mvl0lYuznNoRq5Ck+So5yRmrRRDCsNMR88B13CdD7SvOWI07pg8DOYLf7k73lhFvd4TavvoP</diagram></mxfile>
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0" version="8.6.4" editor="www.draw.io" type="device"><diagram id="82652b06-2c2b-a339-9c87-b7b8398097a5" name="Page-1">7Vpbc6M2FP41flwPd+PHtTeb7UyacZt0uvuUkUEGdQVihBzb++srgYQB4YQm+JImyWQCB13gO993dHQZ2fNke01BFv9OQohHlhFuR/aXkWWZjm3wf8KyKy1TyywNEUWhLLQ33KFfUBplvWiNQpg3CjJCMENZ0xiQNIUBa9gApWTTLLYiuNlrBiKoGe4CgHXr3yhkcWn1XWNv/wZRFKueTUM+WYLgZ0TJOpX9jSx7VfyUjxOg2pLl8xiEZFMz2Vcje04JYeVVsp1DLLBVsJX1vh54Wr03hSnrU2FSVngEeC0//fb6t9vv8uXYTgHyCClDHJ8bsIR4QXLEEEn5oyVhjCQjewbyrHTECm0hb3wWswTzW5NfqsqfMYpEJUYybs0ZJT/hnGBCuS0lKe9qRtYMo5RblV8NUTIGmXiPZBsJuo0DxCjajjNKtruHHFLePi+lf7pEQ3QPtzWThOIakgQyuuNF5NOKf5K2lueW95s9CSbeWBrjGgOmsiKQxIuqtvfg8wuJ/wHneZoz/uJfl2vOwMIJ/Is3MWLwLgOBsG84NE3YUVKyHCUR7zrAiGPD+NU1BPTBtPwt/xtnacSLrhDGyhOcsiGA/irQfMSfeIEPl6th0LanTbRNJa8a2qYSSh1sbwCwbVMD+/rPxVzDGoY8MMhbQllMIpICfLW3zgq1C8oXXK3hD7eIfRdmTpjy7ocsxDGju9ojcftD1soZ99JnEcO4IcAgz1GgzF8RVo3/AxnbycAJ1owI7VSvd0OEwopyB/2UkzUNoOK9jLGARlAWs6alTQDwpDcpxIChx2bkfI1rLN01C0oYCQgv5WH+frMl5VeRuAIhyNg7EoljnFAkrn0cPfiThiIqfSwgRfwlRTgvA38/LcA01Atx49HkYutymZxLLa6jqWXkzjIlGPeL5kLOPNb0DIU5+gWWRQGBe0ZQyop3cmeiBS4zOXQHHJditG2P6QkKw8L9he5mVRpUk4VMhLpAVkTTdFGlePLtGmlSl14+GWPDNOQI3Rts2dxCfHetCFmtcu7gtjeqXvuFM1tz0BwjKADWxIV5/MlhLeUJMFmHz0exVmiCZujCSVdomnoTG3jDhKZ2sjTpGZn8ASJTR+LKs1IWUXj3x42Ga4XmjieXYcHfZwBdlkHsZnkAYY6vHzpdCPvW0vYGQtj1WhD7OsTTYyVI1keCtI/4Uz3iq3T9DAnSVHPNfYzS6N0kQV06ON5MwdUHWE0EXRnIAaLWBVDRvE7ykvO9eDnRaamKPUvLGnZuB3TK9tIxVE2p7Vb+2nZJ+TmyVn2lot2Q32rIbDVUgqA19IIR29b19fZSqoq2r06peEZlGX4D/E/m68gxaIJl6+smFy3QvhPr0wjUbAnUm7xQoGZLoJ51PIH6mse/3d8v7t6ELhVbB5nquPYrpzrbzgrD6FLP0bu9dEHi7J3UfYjzkNsdffnuFB5Xkwk1sfiPXOjK8Hsv6nyQ4SAZ9MWPdzBP79rJONY83elY/7t8uak4W5eb4sqFyM21mk612yrpKzd38kxDA8pNn6vefr7XR9xSDWpr22q6vb3dusJkE8ScLOMQUZ6fPISAgQPSbMlv5YrfzuWB4mcg+bluA2DH1Ldtqx36OlPsAfTnTXS5DbAk1mt/pBKoqQs0BHlc9KCtmcmt9vqCmXGGXRTH10OAd7Y1NUefUrxD5bimPnAdTTlOxzJLr5GrQy/6MPTk4CVu22I6LJjDi8y9iN61u64C9YWMdW8xtVSffnr6NJjSFZf7xb/LokA73XGcF1JgYrQaaseKASngH2XsbWxHNRLhjnG2M5aceLxtELIX+dyOtQ81BJ7hDEPHMURCE4D5p1PNxf+TTS1NJ6c82ePpa+bzdS6OcmonrAKS5uvkLEesVn4Ag05HLH3XcZ8Mta84YjXtmD4M5Ah+uz/cW0a8/Qlq++pf</diagram></mxfile>
+7 -7
View File
@@ -4,9 +4,9 @@ Testing environment to be determined.
### Message publishing
In this scenario, large number of requests are sent to HTTP adapter service
every second. This test checks how much time HTTP adapter took to response to
each request.
In this scenario, large number of requests are sent to HTTP adapter service
every second. This test checks how much time HTTP adapter needs to respond
to each request.
#### Results
@@ -14,10 +14,10 @@ TBD
### Create and get client
In this scenario, large number of requests are sent to manager service to create
client, and than to retrieve its data. This test checks how much time manager
service took to response to each request.
In this scenario, large number of requests are sent to things service to create
things, and than to retrieve their data. This test checks how much time things
service needs to respond to each request.
#### Results
TBD
TBD
+4 -4
View File
@@ -10,9 +10,9 @@ default values.
| Variable | Description | Default |
|----------------------|---------------------|-----------------------|
| MF_CLIENTS_URL | Clients service URL | localhost:8181 |
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_HTTP_ADAPTER_PORT | Service HTTP port | 8180 |
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_THINGS_URL | Things service URL | localhost:8181 |
## Deployment
@@ -28,7 +28,7 @@ services:
ports:
- [host machine port]:8180
environment:
MF_CLIENTS_URL: [Clients service URL]
MF_THINGS_URL: [Things service URL]
MF_NATS_URL: [NATS instance URL]
MF_HTTP_ADAPTER_PORT: [Service HTTP port]
```
@@ -48,7 +48,7 @@ make http
make install
# set the environment variables and run the service
MF_CLIENTS_URL=[Clients service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-http
MF_THINGS_URL=[Things service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-http
```
## Usage
+39 -23
View File
@@ -15,27 +15,16 @@ import (
"github.com/stretchr/testify/assert"
)
const (
id = "123e4567-e89b-12d3-a456-000000000001"
token = "auth_token"
invalidToken = "invalid_token"
msg = `[{"n":"current","t":-1,"v":1.6}]`
)
func newService() mainflux.MessagePublisher {
pub := mocks.NewPublisher()
return adapter.New(pub)
}
func newHTTPServer(pub mainflux.MessagePublisher, cc mainflux.ClientsServiceClient) *httptest.Server {
func newHTTPServer(pub mainflux.MessagePublisher, cc mainflux.ThingsServiceClient) *httptest.Server {
mux := api.MakeHandler(pub, cc)
return httptest.NewServer(mux)
}
func newClientsClient() mainflux.ClientsServiceClient {
return mocks.NewClientsClient(map[string]string{token: id})
}
type testRequest struct {
client *http.Client
method string
@@ -60,32 +49,59 @@ func (tr testRequest) make() (*http.Response, error) {
}
func TestPublish(t *testing.T) {
clientsClient := newClientsClient()
id := uint64(1)
contentType := "application/senml+json"
token := "auth_token"
invalidToken := "invalid_token"
msg := `[{"n":"current","t":-1,"v":1.6}]`
thingsClient := mocks.NewThingsClient(map[string]uint64{token: id})
pub := newService()
ts := newHTTPServer(pub, clientsClient)
ts := newHTTPServer(pub, thingsClient)
defer ts.Close()
client := ts.Client()
cases := map[string]struct {
chanID string
chanID uint64
msg string
contentType string
auth string
status int
}{
"publish message": {id, msg, "application/senml+json", token, http.StatusAccepted},
"publish message with no authorization token": {id, msg, "application/senml+json", "", http.StatusForbidden},
"publish message with invalid authorization token": {id, msg, "application/senml+json", invalidToken, http.StatusForbidden},
"publish message with no content type": {id, msg, "", token, http.StatusAccepted},
"publish message with invalid channel id": {"1", msg, "application/senml+json", token, http.StatusNotFound},
"publish message": {
chanID: id,
msg: msg,
contentType: contentType,
auth: token,
status: http.StatusAccepted,
},
"publish message without authorization token": {
chanID: id,
msg: msg,
contentType: contentType,
auth: "",
status: http.StatusForbidden,
},
"publish message with invalid authorization token": {
chanID: id,
msg: msg,
contentType: contentType,
auth: invalidToken,
status: http.StatusForbidden,
},
"publish message without content type": {
chanID: id,
msg: msg,
contentType: "",
auth: token,
status: http.StatusAccepted,
},
}
for desc, tc := range cases {
req := testRequest{
client: client,
client: ts.Client(),
method: http.MethodPost,
url: fmt.Sprintf("%s/channels/%s/messages", ts.URL, tc.chanID),
url: fmt.Sprintf("%s/channels/%d/messages", ts.URL, tc.chanID),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.msg),
+18 -14
View File
@@ -8,26 +8,25 @@ import (
"net/http"
"time"
"github.com/asaskevich/govalidator"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/things"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const protocol string = "http"
const protocol = "http"
var (
errMalformedData = errors.New("malformed SenML data")
errNotFound = errors.New("non-existent entity")
auth mainflux.ClientsServiceClient
auth mainflux.ThingsServiceClient
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc mainflux.MessagePublisher, cc mainflux.ClientsServiceClient) http.Handler {
func MakeHandler(svc mainflux.MessagePublisher, cc mainflux.ThingsServiceClient) http.Handler {
auth = cc
opts := []kithttp.ServerOption{
@@ -60,36 +59,41 @@ func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
return nil, err
}
channel, err := things.FromString(bone.GetValue(r, "id"))
if err != nil {
return nil, err
}
msg := mainflux.RawMessage{
Publisher: publisher,
Protocol: protocol,
ContentType: r.Header.Get("Content-Type"),
Channel: bone.GetValue(r, "id"),
Channel: channel,
Payload: payload,
}
return msg, nil
}
func authorize(r *http.Request) (string, error) {
func authorize(r *http.Request) (uint64, error) {
apiKey := r.Header.Get("Authorization")
if apiKey == "" {
return "", clients.ErrUnauthorizedAccess
return 0, things.ErrUnauthorizedAccess
}
// extract ID from /channels/:id/messages
c := bone.GetValue(r, "id")
if !govalidator.IsUUID(c) {
return "", errNotFound
chanID, err := things.FromString(bone.GetValue(r, "id"))
if err != nil {
return 0, things.ErrNotFound
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: apiKey, ChanID: c})
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: apiKey, ChanID: chanID})
if err != nil {
return "", err
return 0, err
}
return id.GetValue(), nil
@@ -116,7 +120,7 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.WriteHeader(http.StatusBadRequest)
case errNotFound:
w.WriteHeader(http.StatusNotFound)
case clients.ErrUnauthorizedAccess:
case things.ErrUnauthorizedAccess:
w.WriteHeader(http.StatusForbidden)
default:
if e, ok := status.FromError(err); ok {
-34
View File
@@ -1,34 +0,0 @@
package mocks
import (
"context"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
"google.golang.org/grpc"
)
var _ mainflux.ClientsServiceClient = (*clientsClient)(nil)
type clientsClient struct {
clients map[string]string
}
// NewClientsClient returns mock implementation of clients service client.
func NewClientsClient(data map[string]string) mainflux.ClientsServiceClient {
return &clientsClient{data}
}
func (client clientsClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, opts ...grpc.CallOption) (*mainflux.Identity, error) {
key := req.GetToken()
if key == "" {
return nil, clients.ErrUnauthorizedAccess
}
id, ok := client.clients[key]
if !ok {
return nil, clients.ErrUnauthorizedAccess
}
return &mainflux.Identity{Value: id}, nil
}
+38
View File
@@ -0,0 +1,38 @@
package mocks
import (
"context"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/things"
"google.golang.org/grpc"
)
var _ mainflux.ThingsServiceClient = (*thingsClient)(nil)
type thingsClient struct {
things map[string]uint64
}
// NewThingsClient returns mock implementation of things service client.
func NewThingsClient(data map[string]uint64) mainflux.ThingsServiceClient {
return &thingsClient{data}
}
func (tc thingsClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, opts ...grpc.CallOption) (*mainflux.ThingID, error) {
key := req.GetToken()
if key == "" {
return nil, things.ErrUnauthorizedAccess
}
id, ok := tc.things[key]
if !ok {
return nil, things.ErrUnauthorizedAccess
}
return &mainflux.ThingID{Value: id}, nil
}
func (tc thingsClient) Identify(ctx context.Context, req *mainflux.Token, opts ...grpc.CallOption) (*mainflux.ThingID, error) {
return nil, nil
}
+1 -1
View File
@@ -26,6 +26,6 @@ func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
return err
}
subject := fmt.Sprintf("channel.%s", msg.Channel)
subject := fmt.Sprintf("channel.%d", msg.Channel)
return pub.nc.Publish(subject, data)
}
+10 -5
View File
@@ -2,23 +2,28 @@ syntax = "proto3";
package mainflux;
service ClientsService {
rpc CanAccess(AccessReq) returns (Identity) {}
service ThingsService {
rpc CanAccess(AccessReq) returns (ThingID) {}
rpc Identify(Token) returns (ThingID) {}
}
service UsersService {
rpc Identify(Token) returns (mainflux.Identity) {}
rpc Identify(Token) returns (UserID) {}
}
message AccessReq {
string token = 1;
string chanID = 2;
uint64 chanID = 2;
}
message ThingID {
uint64 value = 1;
}
message Token {
string value = 1;
}
message Identity {
message UserID {
string value = 1;
}

Some files were not shown because too many files have changed in this diff Show More