mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50e14f0c65 | |||
| d2bce84c06 | |||
| 89aa9603ec | |||
| 2ae581368f | |||
| 99a3ec5d2d | |||
| 89a0fa3e9c | |||
| 1bc4dc9575 | |||
| bca3d0774f | |||
| 816c172823 | |||
| 6a361209c8 | |||
| 27e1be23f5 | |||
| c9bf245010 | |||
| ff30957614 | |||
| db6165aa6b | |||
| c088e664c3 | |||
| d499068e43 | |||
| 38d4c3fcea | |||
| aea7db14b7 | |||
| 58f3c735a0 | |||
| 789963447a |
Generated
+139
-102
@@ -32,13 +32,13 @@
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
revision = "61153c768f31ee5f130071d08fc82b85208528de"
|
||||
version = "v1.1.0"
|
||||
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -50,7 +50,7 @@
|
||||
branch = "master"
|
||||
name = "github.com/containerd/continuity"
|
||||
packages = ["pathdriver"]
|
||||
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
|
||||
revision = "c6cef34830231743494fe2969284df7b82cc0ad0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
@@ -70,38 +70,6 @@
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/docker/docker"
|
||||
packages = [
|
||||
"api/types",
|
||||
"api/types/blkiodev",
|
||||
"api/types/container",
|
||||
"api/types/filters",
|
||||
"api/types/mount",
|
||||
"api/types/network",
|
||||
"api/types/registry",
|
||||
"api/types/strslice",
|
||||
"api/types/swarm",
|
||||
"api/types/swarm/runtime",
|
||||
"api/types/versions",
|
||||
"opts",
|
||||
"pkg/archive",
|
||||
"pkg/fileutils",
|
||||
"pkg/homedir",
|
||||
"pkg/idtools",
|
||||
"pkg/ioutils",
|
||||
"pkg/jsonmessage",
|
||||
"pkg/longpath",
|
||||
"pkg/mount",
|
||||
"pkg/pools",
|
||||
"pkg/stdcopy",
|
||||
"pkg/system",
|
||||
"pkg/term",
|
||||
"pkg/term/windows"
|
||||
]
|
||||
revision = "72ba7f593fa4dbb628cf5ee83cd7daf955934cf5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
@@ -111,8 +79,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
|
||||
version = "v0.3.2"
|
||||
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
|
||||
version = "v0.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -120,12 +88,6 @@
|
||||
packages = ["."]
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsouza/go-dockerclient"
|
||||
packages = ["."]
|
||||
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
@@ -134,6 +96,7 @@
|
||||
"metrics",
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"transport/grpc",
|
||||
"transport/http"
|
||||
]
|
||||
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
|
||||
@@ -165,9 +128,15 @@
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
|
||||
version = "v1.0.0"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
@@ -175,21 +144,6 @@
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jinzhu/gorm"
|
||||
packages = [
|
||||
".",
|
||||
"dialects/postgres"
|
||||
]
|
||||
revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166"
|
||||
version = "v1.9.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/inflection"
|
||||
packages = ["."]
|
||||
revision = "04140366298a54a039076d798123ffa108fff46c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/logfmt"
|
||||
@@ -201,10 +155,9 @@
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"hstore",
|
||||
"oid"
|
||||
]
|
||||
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
|
||||
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
@@ -219,8 +172,8 @@
|
||||
"encoders/builtin",
|
||||
"util"
|
||||
]
|
||||
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
|
||||
version = "v1.4.0"
|
||||
revision = "062418ea1c2181f52dc0f954f6204370519a868b"
|
||||
version = "v1.5.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nats-io/nuid"
|
||||
@@ -252,6 +205,37 @@
|
||||
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/ory/dockertest"
|
||||
packages = [
|
||||
"docker",
|
||||
"docker/opts",
|
||||
"docker/pkg/archive",
|
||||
"docker/pkg/fileutils",
|
||||
"docker/pkg/homedir",
|
||||
"docker/pkg/idtools",
|
||||
"docker/pkg/ioutils",
|
||||
"docker/pkg/jsonmessage",
|
||||
"docker/pkg/longpath",
|
||||
"docker/pkg/mount",
|
||||
"docker/pkg/pools",
|
||||
"docker/pkg/stdcopy",
|
||||
"docker/pkg/system",
|
||||
"docker/pkg/term",
|
||||
"docker/pkg/term/windows",
|
||||
"docker/types",
|
||||
"docker/types/blkiodev",
|
||||
"docker/types/container",
|
||||
"docker/types/filters",
|
||||
"docker/types/mount",
|
||||
"docker/types/network",
|
||||
"docker/types/registry",
|
||||
"docker/types/strslice",
|
||||
"docker/types/versions"
|
||||
]
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
@@ -287,7 +271,7 @@
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
|
||||
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -298,7 +282,16 @@
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
|
||||
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
packages = [
|
||||
".",
|
||||
"sqlparse"
|
||||
]
|
||||
revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
@@ -327,33 +320,8 @@
|
||||
[[projects]]
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "8474b86a5a6f79c443ce4b2992817ff32cf208b8"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"internal/bufferpool",
|
||||
"internal/color",
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "35aad584952c3e7020db7b839f6b102de6271f89"
|
||||
version = "v1.7.1"
|
||||
revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -363,16 +331,21 @@
|
||||
"blowfish",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "374053ea96cb300f8671b8d3b07edeeb06e203b4"
|
||||
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/timeseries",
|
||||
"trace"
|
||||
]
|
||||
revision = "24dd3780ca4f75fed9f321890729414a4b5d3f13"
|
||||
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -381,17 +354,81 @@
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "01acb38716e021ed1fc03a602bdb5838e1358c5e"
|
||||
revision = "7dfd1290c7917b7ba22824b9d24954ab3002fe24"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"channelz",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclb/grpc_lb_v1/messages",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "41344da2231b913fa3d983840a57a6b1b7b631a1"
|
||||
version = "v1.12.0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/gorp.v1"
|
||||
packages = ["."]
|
||||
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
|
||||
version = "v3.1.6"
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "c1b28e90e21e838cec2d16c95d8eb414b5de9bbe9667acb01b5375db047cc6db"
|
||||
inputs-digest = "cce4c6cd08b6213615afc135d8e7d4f255d4ee32b42062e1f3208de246366418"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+5
-5
@@ -28,7 +28,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jinzhu/gorm"
|
||||
@@ -58,10 +58,6 @@
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "go.uber.org/zap"
|
||||
version = "1.7.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
@@ -74,6 +70,10 @@
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
BUILD_DIR = build
|
||||
SERVICES = manager http normalizer ws
|
||||
SERVICES = users clients http normalizer ws coap
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOOS ?= linux
|
||||
@@ -23,7 +23,7 @@ install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
proto:
|
||||
protoc --go_out=. *.proto
|
||||
protoc --go_out=plugins=grpc:. *.proto
|
||||
|
||||
$(SERVICES): proto
|
||||
$(call compile_service,$(@))
|
||||
@@ -32,11 +32,15 @@ $(DOCKERS):
|
||||
$(call make_docker,$(@))
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
|
||||
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile mqtt
|
||||
|
||||
latest: dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc; \
|
||||
done
|
||||
docker push mainflux/dashflux
|
||||
docker push mainflux/mqtt
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
@@ -46,3 +50,7 @@ release:
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker push mainflux/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version)
|
||||
docker push mainflux/dashflux:$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt:$(version)
|
||||
docker push mainflux/mqtt:$(version)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package mainflux
|
||||
|
||||
// Response contains HTTP response specific methods.
|
||||
type Response interface {
|
||||
// Code returns HTTP response code.
|
||||
Code() int
|
||||
|
||||
// Headers returns map of HTTP headers with their values.
|
||||
Headers() map[string]string
|
||||
|
||||
// Empty indicates if HTTP response has content.
|
||||
Empty() bool
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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
|
||||
@@ -0,0 +1,49 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package grpc contains implementation of clients service gRPC API.
|
||||
package grpc
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package grpc
|
||||
|
||||
type accessRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package http contains implementation of clients service HTTP API.
|
||||
package http
|
||||
@@ -1,43 +1,13 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
func registrationEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(userReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := svc.Register(req.user)
|
||||
return tokenRes{}, err
|
||||
}
|
||||
}
|
||||
|
||||
func loginEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(userReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := svc.Login(req.user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tokenRes{token}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func addClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func addClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addClientReq)
|
||||
|
||||
@@ -54,7 +24,7 @@ func addClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func updateClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func updateClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateClientReq)
|
||||
|
||||
@@ -72,7 +42,7 @@ func updateClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func viewClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func viewClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
@@ -89,7 +59,7 @@ func viewClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func listClientsEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listResourcesReq)
|
||||
|
||||
@@ -106,12 +76,12 @@ func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func removeClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
err := req.validate()
|
||||
if err == manager.ErrNotFound {
|
||||
if err == clients.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
|
||||
@@ -127,7 +97,7 @@ func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func createChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createChannelReq)
|
||||
|
||||
@@ -144,7 +114,7 @@ func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func updateChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateChannelReq)
|
||||
|
||||
@@ -162,7 +132,7 @@ func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func viewChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
@@ -179,7 +149,7 @@ func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func listChannelsEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listResourcesReq)
|
||||
|
||||
@@ -196,12 +166,12 @@ func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func removeChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
if err == manager.ErrNotFound {
|
||||
if err == clients.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
return nil, err
|
||||
@@ -214,7 +184,7 @@ func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func connectEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
@@ -222,7 +192,7 @@ func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
if err := svc.Connect(cr.key, cr.chanID, cr.clientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -230,7 +200,7 @@ func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
func disconnectEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
@@ -238,44 +208,10 @@ func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
if err := svc.Disconnect(cr.key, cr.chanID, cr.clientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identityEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, err := svc.Identity(req.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func canAccessEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, err := svc.CanAccess(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
@@ -11,21 +11,13 @@ type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type userReq struct {
|
||||
user manager.User
|
||||
}
|
||||
|
||||
func (req userReq) validate() error {
|
||||
return req.user.Validate()
|
||||
}
|
||||
|
||||
type identityReq struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -33,12 +25,12 @@ func (req identityReq) validate() error {
|
||||
|
||||
type addClientReq struct {
|
||||
key string
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req addClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
@@ -47,16 +39,16 @@ func (req addClientReq) validate() error {
|
||||
type updateClientReq struct {
|
||||
key string
|
||||
id string
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req updateClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
@@ -64,12 +56,12 @@ func (req updateClientReq) validate() error {
|
||||
|
||||
type createChannelReq struct {
|
||||
key string
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -78,16 +70,16 @@ func (req createChannelReq) validate() error {
|
||||
type updateChannelReq struct {
|
||||
key string
|
||||
id string
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -100,11 +92,11 @@ type viewResourceReq struct {
|
||||
|
||||
func (req viewResourceReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -118,29 +110,29 @@ type listResourcesReq struct {
|
||||
|
||||
func (req *listResourcesReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.ErrMalformedEntity
|
||||
return clients.ErrMalformedEntity
|
||||
}
|
||||
|
||||
type connectionReq struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
chanID string
|
||||
clientID string
|
||||
}
|
||||
|
||||
func (req connectionReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanId) || !govalidator.IsUUID(req.clientId) {
|
||||
return manager.ErrNotFound
|
||||
if !govalidator.IsUUID(req.chanID) || !govalidator.IsUUID(req.clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1,10 +1,10 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -12,35 +12,17 @@ import (
|
||||
const wrong string = "?"
|
||||
|
||||
var (
|
||||
client = manager.Client{Type: "app"}
|
||||
channel = manager.Channel{}
|
||||
client = clients.Client{Type: "app"}
|
||||
channel = clients.Channel{}
|
||||
)
|
||||
|
||||
func TestUserReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"valid user request": {manager.User{"foo@example.com", "pass"}, nil},
|
||||
"malformed e-mail": {manager.User{wrong, "pass"}, manager.ErrMalformedEntity},
|
||||
"empty e-mail": {manager.User{"", "pass"}, manager.ErrMalformedEntity},
|
||||
"empty password": {manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := userReq{tc.user}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"non-empty token": {uuid.NewV4().String(), nil},
|
||||
"empty token": {"", manager.ErrUnauthorizedAccess},
|
||||
"empty token": {"", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@@ -54,13 +36,13 @@ func TestAddClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client addition request": {client, key, nil},
|
||||
"missing token": {client, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: wrong}, key, manager.ErrMalformedEntity},
|
||||
"missing token": {client, "", clients.ErrUnauthorizedAccess},
|
||||
"wrong client type": {clients.Client{Type: wrong}, key, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@@ -79,15 +61,15 @@ func TestUpdateClientReqValidation(t *testing.T) {
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
client clients.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client update request": {client, id, key, nil},
|
||||
"non-uuid client ID": {client, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {client, id, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: "invalid"}, id, key, manager.ErrMalformedEntity},
|
||||
"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 {
|
||||
@@ -106,12 +88,12 @@ func TestCreateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel creation request": {channel, key, nil},
|
||||
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
|
||||
"missing token": {channel, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@@ -130,14 +112,14 @@ func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
channel clients.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel update request": {channel, id, key, nil},
|
||||
"non-uuid channel ID": {channel, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {channel, id, "", manager.ErrUnauthorizedAccess},
|
||||
"non-uuid channel ID": {channel, wrong, key, clients.ErrNotFound},
|
||||
"missing token": {channel, id, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@@ -162,8 +144,8 @@ func TestViewResourceReqValidation(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"valid resource viewing request": {id, key, nil},
|
||||
"missing token": {id, "", manager.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
|
||||
"missing token": {id, "", clients.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
@@ -184,11 +166,11 @@ func TestListResourcesReqValidation(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
"valid listing request": {key, value, value, nil},
|
||||
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
|
||||
"negative offset": {key, -value, value, manager.ErrMalformedEntity},
|
||||
"zero limit": {key, value, 0, manager.ErrMalformedEntity},
|
||||
"negative limit": {key, value, -value, manager.ErrMalformedEntity},
|
||||
"too big limit": {key, value, 20 * value, manager.ErrMalformedEntity},
|
||||
"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 {
|
||||
@@ -0,0 +1,201 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*identityRes)(nil)
|
||||
_ mainflux.Response = (*removeRes)(nil)
|
||||
_ mainflux.Response = (*clientRes)(nil)
|
||||
_ mainflux.Response = (*viewClientRes)(nil)
|
||||
_ mainflux.Response = (*listClientsRes)(nil)
|
||||
_ mainflux.Response = (*channelRes)(nil)
|
||||
_ mainflux.Response = (*viewChannelRes)(nil)
|
||||
_ mainflux.Response = (*listChannelsRes)(nil)
|
||||
_ mainflux.Response = (*connectionRes)(nil)
|
||||
_ mainflux.Response = (*disconnectionRes)(nil)
|
||||
)
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (res identityRes) Headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-client-id": res.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (res identityRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res identityRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type clientRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res clientRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res clientRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/clients/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res clientRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewClientRes struct {
|
||||
clients.Client
|
||||
}
|
||||
|
||||
func (res viewClientRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewClientRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewClientRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listClientsRes struct {
|
||||
Clients []clients.Client `json:"clients"`
|
||||
}
|
||||
|
||||
func (res listClientsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listClientsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res channelRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/channels/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
clients.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listChannelsRes struct {
|
||||
Channels []clients.Channel `json:"channels"`
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type connectionRes struct{}
|
||||
|
||||
func (res connectionRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res connectionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectionRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectionRes struct{}
|
||||
|
||||
func (res disconnectionRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectionRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package api
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@@ -24,27 +24,13 @@ var (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc manager.Service) http.Handler {
|
||||
func MakeHandler(svc clients.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/users", kithttp.NewServer(
|
||||
registrationEndpoint(svc),
|
||||
decodeCredentials,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/tokens", kithttp.NewServer(
|
||||
loginEndpoint(svc),
|
||||
decodeCredentials,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/clients", kithttp.NewServer(
|
||||
addClientEndpoint(svc),
|
||||
decodeClientCreation,
|
||||
@@ -129,53 +115,18 @@ func MakeHandler(svc manager.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/access-grant", kithttp.NewServer(
|
||||
identityEndpoint(svc),
|
||||
decodeIdentity,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/channels/:id/access-grant", kithttp.NewServer(
|
||||
canAccessEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version())
|
||||
r.GetFunc("/version", mainflux.Version("clients"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeIdentity(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := identityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var user manager.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userReq{user}, nil
|
||||
}
|
||||
|
||||
func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client manager.Client
|
||||
var client clients.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -193,7 +144,7 @@ func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error)
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client manager.Client
|
||||
var client clients.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,7 +163,7 @@ func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, err
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel manager.Channel
|
||||
var channel clients.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -230,7 +181,7 @@ func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel manager.Channel
|
||||
var channel clients.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -292,8 +243,8 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := connectionReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
chanId: bone.GetValue(r, "chanId"),
|
||||
clientId: bone.GetValue(r, "clientId"),
|
||||
chanID: bone.GetValue(r, "chanId"),
|
||||
clientID: bone.GetValue(r, "clientId"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -302,14 +253,14 @@ func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(apiRes); ok {
|
||||
for k, v := range ar.headers() {
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.code())
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.empty() {
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -321,13 +272,13 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case manager.ErrMalformedEntity:
|
||||
case clients.ErrMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case manager.ErrUnauthorizedAccess:
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case manager.ErrNotFound:
|
||||
case clients.ErrNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case manager.ErrConflict:
|
||||
case clients.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
@@ -6,50 +6,23 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*loggingMiddleware)(nil)
|
||||
var _ clients.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc manager.Service
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
|
||||
func LoggingMiddleware(svc clients.Service, logger log.Logger) clients.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method register for user %s took %s to complete", user.Email, 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.Register(user)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method login for user %s took %s to complete", user.Email, 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.Login(user)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
|
||||
func (lm *loggingMiddleware) AddClient(key string, client clients.Client) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -62,7 +35,7 @@ func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id st
|
||||
return lm.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client clients.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_client for key %s and client %s took %s to complete", key, client.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -75,7 +48,7 @@ func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (er
|
||||
return lm.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client clients.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -88,7 +61,7 @@ func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.C
|
||||
return lm.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListClients(key string, offset, limit int) (clients []manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ListClients(key string, offset, limit int) (clients []clients.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_clients for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -114,7 +87,7 @@ func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
return lm.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel clients.Channel) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -127,7 +100,7 @@ func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel)
|
||||
return lm.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel clients.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_channel for key %s and channel %s took %s to complete", key, channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -140,7 +113,7 @@ func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel)
|
||||
return lm.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel clients.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -153,7 +126,7 @@ func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager
|
||||
return lm.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannels(key string, offset, limit int) (channels []manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ListChannels(key string, offset, limit int) (channels []clients.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_channels for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -205,19 +178,6 @@ func (lm *loggingMiddleware) Disconnect(key, chanID, clientID string) (err error
|
||||
return lm.svc.Disconnect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method identity for client %s took %s to complete", id, 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.Identity(key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method can_access for key %s, channel %s and publisher %s took %s to complete", key, id, pub, time.Since(begin))
|
||||
@@ -6,20 +6,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*metricsMiddleware)(nil)
|
||||
var _ clients.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc manager.Service
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
|
||||
func MetricsMiddleware(svc clients.Service, counter metrics.Counter, latency metrics.Histogram) clients.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
@@ -27,25 +27,7 @@ func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency met
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Register(user manager.User) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "register").Add(1)
|
||||
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Register(user)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Login(user manager.User) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "login").Add(1)
|
||||
ms.latency.With("method", "login").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Login(user)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (string, error) {
|
||||
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())
|
||||
@@ -54,7 +36,7 @@ func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (strin
|
||||
return ms.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
|
||||
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())
|
||||
@@ -63,7 +45,7 @@ func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) err
|
||||
return ms.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
|
||||
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())
|
||||
@@ -72,7 +54,7 @@ func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client,
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]manager.Client, error) {
|
||||
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())
|
||||
@@ -90,7 +72,7 @@ func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
return ms.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel) (string, error) {
|
||||
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())
|
||||
@@ -99,7 +81,7 @@ func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel)
|
||||
return ms.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
|
||||
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())
|
||||
@@ -108,7 +90,7 @@ func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel)
|
||||
return ms.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
|
||||
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())
|
||||
@@ -117,7 +99,7 @@ func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]manager.Channel, error) {
|
||||
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())
|
||||
@@ -135,31 +117,22 @@ func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
return ms.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Connect(key, chanId, clientId string) error {
|
||||
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)
|
||||
return ms.svc.Connect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Disconnect(key, chanId, clientId string) error {
|
||||
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) Identity(key string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identity").Add(1)
|
||||
ms.latency.With("method", "identity").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identity(key)
|
||||
return ms.svc.Disconnect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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))
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package manager
|
||||
package clients
|
||||
|
||||
// Channel represents a Mainflux "communication group". This group contains the
|
||||
// clients that can exchange messages between eachother.
|
||||
type Channel struct {
|
||||
ID string `gorm:"type:char(36);primary_key" json:"id"`
|
||||
Owner string `gorm:"type:varchar(254);not null" json:"-"`
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"-"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Clients []Client `gorm:"many2many:channel_clients" json:"connected,omitempty"`
|
||||
Clients []Client `json:"connected,omitempty"`
|
||||
}
|
||||
|
||||
// ChannelRepository specifies a channel persistence API.
|
||||
@@ -1,19 +1,19 @@
|
||||
package manager
|
||||
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 `gorm:"type:char(36);primary_key" json:"id"`
|
||||
Owner string `gorm:"type:varchar(254);not null" json:"-"`
|
||||
Type string `gorm:"type:varchar(10);not null" json:"type"`
|
||||
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 = map[string]bool{
|
||||
var clientTypes = map[string]bool{
|
||||
"app": true,
|
||||
"device": true,
|
||||
}
|
||||
@@ -29,8 +29,8 @@ func (c *Client) Validate() error {
|
||||
|
||||
// ClientRepository specifies a client persistence API.
|
||||
type ClientRepository interface {
|
||||
// Id generates new resource identifier.
|
||||
Id() string
|
||||
// ID generates new resource identifier.
|
||||
ID() string
|
||||
|
||||
// Save persists the client. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package clients contains the domain concept definitions needed to support
|
||||
// Mainflux clients service functionality.
|
||||
package clients
|
||||
@@ -1,10 +1,11 @@
|
||||
// Package jwt provides a JWT identity provider.
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,14 +13,14 @@ const (
|
||||
duration time.Duration = 10 * time.Hour
|
||||
)
|
||||
|
||||
var _ manager.IdentityProvider = (*jwtIdentityProvider)(nil)
|
||||
var _ clients.IdentityProvider = (*jwtIdentityProvider)(nil)
|
||||
|
||||
type jwtIdentityProvider struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// New instantiates a JWT identity provider.
|
||||
func New(secret string) manager.IdentityProvider {
|
||||
func New(secret string) clients.IdentityProvider {
|
||||
return &jwtIdentityProvider{secret}
|
||||
}
|
||||
|
||||
@@ -55,19 +56,19 @@ func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
|
||||
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, manager.ErrUnauthorizedAccess
|
||||
return nil, clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return []byte(idp.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims["sub"].(string), nil
|
||||
}
|
||||
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -5,75 +5,75 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
var _ clients.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
|
||||
const chanId = "123e4567-e89b-12d3-a456-"
|
||||
const chanID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type channelRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
channels map[string]manager.Channel
|
||||
clients manager.ClientRepository
|
||||
channels map[string]clients.Channel
|
||||
clients clients.ClientRepository
|
||||
}
|
||||
|
||||
// NewChannelRepository creates in-memory channel repository.
|
||||
func NewChannelRepository(clients manager.ClientRepository) manager.ChannelRepository {
|
||||
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
|
||||
return &channelRepositoryMock{
|
||||
channels: make(map[string]manager.Channel),
|
||||
clients: clients,
|
||||
channels: make(map[string]clients.Channel),
|
||||
clients: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
|
||||
func (crm *channelRepositoryMock) Save(channel clients.Channel) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
channel.ID = fmt.Sprintf("%s%012d", chanId, crm.counter)
|
||||
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 manager.Channel) error {
|
||||
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 manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.channels[dbKey] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (clients.Channel, error) {
|
||||
if c, ok := crm.channels[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Channel{}, manager.ErrNotFound
|
||||
return clients.Channel{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []manager.Channel {
|
||||
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([]manager.Channel, 0)
|
||||
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)
|
||||
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 {
|
||||
@@ -89,13 +89,13 @@ func (crm *channelRepositoryMock) Remove(owner, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
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)
|
||||
client, err := crm.clients.One(owner, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -103,19 +103,19 @@ func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
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 manager.ErrNotFound
|
||||
if !crm.HasClient(chanID, clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
connected := make([]manager.Client, len(channel.Clients)-1)
|
||||
connected := make([]clients.Client, len(channel.Clients)-1)
|
||||
for _, client := range channel.Clients {
|
||||
if client.ID != clientId {
|
||||
if client.ID != clientID {
|
||||
connected = append(connected, client)
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,35 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
|
||||
const cliId = "123e4567-e89b-12d3-a456-"
|
||||
const cliID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type clientRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
clients map[string]manager.Client
|
||||
clients map[string]clients.Client
|
||||
}
|
||||
|
||||
// NewClientRepository creates in-memory client repository.
|
||||
func NewClientRepository() manager.ClientRepository {
|
||||
func NewClientRepository() clients.ClientRepository {
|
||||
return &clientRepositoryMock{
|
||||
clients: make(map[string]manager.Client),
|
||||
clients: make(map[string]clients.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Id() string {
|
||||
func (crm *clientRepositoryMock) ID() string {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
return fmt.Sprintf("%s%012d", cliId, crm.counter)
|
||||
crm.counter++
|
||||
return fmt.Sprintf("%s%012d", cliID, crm.counter)
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
func (crm *clientRepositoryMock) Save(client clients.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -42,14 +42,14 @@ func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Update(client manager.Client) error {
|
||||
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 manager.ErrNotFound
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.clients[dbKey] = client
|
||||
@@ -57,27 +57,27 @@ func (crm *clientRepositoryMock) Update(client manager.Client) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
|
||||
if c, ok := crm.clients[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Client{}, manager.ErrNotFound
|
||||
return clients.Client{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []manager.Client {
|
||||
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([]manager.Client, 0)
|
||||
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)
|
||||
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 {
|
||||
@@ -1,10 +1,12 @@
|
||||
package mocks
|
||||
|
||||
import "github.com/mainflux/mainflux/manager"
|
||||
import (
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
_ manager.Hasher = (*hasherMock)(nil)
|
||||
_ manager.IdentityProvider = (*identityProviderMock)(nil)
|
||||
_ clients.Hasher = (*hasherMock)(nil)
|
||||
_ clients.IdentityProvider = (*identityProviderMock)(nil)
|
||||
)
|
||||
|
||||
type hasherMock struct{}
|
||||
@@ -15,7 +17,7 @@ func (hm *hasherMock) Hash(pwd string) (string, error) {
|
||||
|
||||
func (hm *hasherMock) Compare(plain, hashed string) error {
|
||||
if plain != hashed {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -25,7 +27,7 @@ type identityProviderMock struct{}
|
||||
|
||||
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
|
||||
if id == "" {
|
||||
return "", manager.ErrUnauthorizedAccess
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return id, nil
|
||||
@@ -41,12 +43,12 @@ func (idp *identityProviderMock) Identity(key string) (string, error) {
|
||||
|
||||
// NewHasher creates "no-op" hasher for test purposes. This implementation will
|
||||
// return secrets without changing them.
|
||||
func NewHasher() manager.Hasher {
|
||||
func NewHasher() clients.Hasher {
|
||||
return &hasherMock{}
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() manager.IdentityProvider {
|
||||
func NewIdentityProvider() clients.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceClient = (*usersServiceMock)(nil)
|
||||
|
||||
type usersServiceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.UsersServiceClient {
|
||||
return &usersServiceMock{users}
|
||||
}
|
||||
|
||||
func (svc usersServiceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
if id, ok := svc.users[in.Value]; ok {
|
||||
return &mainflux.Identity{Value: id}, nil
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance and applies any
|
||||
// unapplied database migrations. A non-nil error is returned to indicate
|
||||
// failure.
|
||||
func Connect(host, port, name, user, pass string) (*sql.DB, error) {
|
||||
t := "host=%s port=%s user=%s dbname=%s password=%s sslmode=disable"
|
||||
url := fmt.Sprintf(t, host, port, user, name, pass)
|
||||
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sql.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
&migrate.Migration{
|
||||
Id: "clients_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE clients (
|
||||
id CHAR(36),
|
||||
owner VARCHAR(254),
|
||||
type VARCHAR(10) NOT NULL,
|
||||
name TEXT,
|
||||
key TEXT,
|
||||
payload TEXT,
|
||||
PRIMARY KEY (id, owner)
|
||||
)`,
|
||||
`CREATE TABLE channels (
|
||||
id CHAR(36),
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
PRIMARY KEY (id, owner)
|
||||
)`,
|
||||
`CREATE TABLE connections (
|
||||
channel_id CHAR(36),
|
||||
channel_owner VARCHAR(254),
|
||||
client_id CHAR(36),
|
||||
client_owner VARCHAR(254),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (id, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (client_id, client_owner) REFERENCES clients (id, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, client_id, client_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE clients",
|
||||
"DROP TABLE channels",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package postgres_test contains tests for PostgreSQL repository
|
||||
// implementations.
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var (
|
||||
testLog = logger.New(os.Stdout)
|
||||
db *sql.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect("localhost", port, "test", "test", "test"); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package clients
|
||||
|
||||
// Hasher specifies an API for generating hashes of an arbitrary textual
|
||||
// content.
|
||||
@@ -0,0 +1,265 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,13 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux manager service
|
||||
description: HTTP API for managing platform users, devices, applications and channels.
|
||||
title: Mainflux clients service
|
||||
description: HTTP API for managing platform devices, applications and channels.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/users:
|
||||
post:
|
||||
summary: Registers user account
|
||||
description: |
|
||||
Registers new user account given email and password. New account will
|
||||
be uniquely identified by its email address.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- name: user
|
||||
description: JSON-formatted document describing the new user.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Registered new user.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using an existing email address.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/tokens:
|
||||
post:
|
||||
summary: User authentication
|
||||
description: |
|
||||
Generates an access token when provided with proper credentials.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- name: credentials
|
||||
description: JSON-formatted document containing user credentials.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: User authenticated.
|
||||
schema:
|
||||
$ref: "#/definitions/Token"
|
||||
400:
|
||||
description: |
|
||||
Failed due to malformed JSON.
|
||||
403:
|
||||
description: |
|
||||
Failed due to using invalid credentials.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/clients:
|
||||
post:
|
||||
summary: Adds new client
|
||||
@@ -358,49 +303,6 @@ paths:
|
||||
description: Channel or client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/access-grant:
|
||||
get:
|
||||
summary: Checks the token validity
|
||||
description: |
|
||||
Internal endpoint used to verify requests forwarded by stateful adapters
|
||||
like MQTT. If the request is made using valid token, an identifier bound
|
||||
to that token will be returned.
|
||||
tags:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
responses:
|
||||
200:
|
||||
description: ID retrieved
|
||||
headers:
|
||||
X-client-id:
|
||||
type: string
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
/channels/{chanId}/access-grant:
|
||||
get:
|
||||
summary: Checks channel accessibility
|
||||
description: |
|
||||
Internal endpoint used to determine whether or not a channel can be
|
||||
accessed using credentials provided as part of the request.
|
||||
tags:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
description: Client can access the channel.
|
||||
headers:
|
||||
X-client-id:
|
||||
type: string
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Client cannot access the channel.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
@@ -541,26 +443,3 @@ definitions:
|
||||
description: Arbitrary, string-encoded client's data.
|
||||
required:
|
||||
- type
|
||||
Token:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: Generated access token.
|
||||
required:
|
||||
- token
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: "test@example.com"
|
||||
description: User's email address will be used as its unique identifier
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: Free-form account password used for acquiring auth token(s).
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
@@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
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"
|
||||
usersapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "clients"
|
||||
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"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envSecret = "MF_CLIENTS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
UsersURL string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
defer db.Close()
|
||||
|
||||
conn := connectToUsersService(cfg.UsersURL, logger)
|
||||
defer conn.Close()
|
||||
|
||||
svc := newService(conn, db, cfg.Secret, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Clients service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, secret string, logger log.Logger) clients.Service {
|
||||
users := usersapi.NewClient(conn)
|
||||
clientsRepo := postgres.NewClientRepository(db, logger)
|
||||
channelsRepo := postgres.NewChannelRepository(db, logger)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
|
||||
svc := clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "clients",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "clients",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc clients.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))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc clients.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))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
||||
+59
-44
@@ -6,44 +6,85 @@ import (
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
port int = 5683
|
||||
defNatsURL string = broker.DefaultURL
|
||||
envNatsURL string = "COAP_ADAPTER_NATS_URL"
|
||||
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 {
|
||||
Port int
|
||||
NatsURL string
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port int
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: defPort,
|
||||
}
|
||||
|
||||
logger, _ := zap.NewProduction()
|
||||
defer logger.Sync() // flushes buffer, if any
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc := connectToNats(cfg, logger)
|
||||
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()
|
||||
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
ca := adapter.NewCoAPAdapter(logger, pub)
|
||||
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()
|
||||
|
||||
nc.Subscribe("src.http", ca.BridgeHandler)
|
||||
nc.Subscribe("src.mqtt", ca.BridgeHandler)
|
||||
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() {
|
||||
coapAddr := fmt.Sprintf(":%d", cfg.Port)
|
||||
errs <- ca.Serve(coapAddr)
|
||||
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() {
|
||||
@@ -52,32 +93,6 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
c := <-errs
|
||||
logger.Info("terminated", zap.String("error", c.Error()))
|
||||
}
|
||||
|
||||
func loadConfig() *config {
|
||||
return &config{
|
||||
NatsURL: env(envNatsURL, defNatsURL),
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func connectToNats(cfg *config, logger *zap.Logger) *broker.Conn {
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to NATS", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nc
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("CoAP adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
+15
-7
@@ -9,33 +9,34 @@ 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"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
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
|
||||
defManagerURL string = "http://localhost:8180"
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envManagerURL string = "MF_MANAGER_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
@@ -49,6 +50,14 @@ func main() {
|
||||
}
|
||||
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)
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
|
||||
svc := adapter.New(pub)
|
||||
@@ -73,9 +82,8 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/bcrypt"
|
||||
"github.com/mainflux/mainflux/manager/jwt"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost string = "localhost"
|
||||
defDBPort string = "5432"
|
||||
defDBUser string = "mainflux"
|
||||
defDBPass string = "mainflux"
|
||||
defDBName string = "manager"
|
||||
defPort string = "8180"
|
||||
defSecret string = "manager"
|
||||
envDBHost string = "MF_DB_HOST"
|
||||
envDBPort string = "MF_DB_PORT"
|
||||
envDBUser string = "MF_DB_USER"
|
||||
envDBPass string = "MF_DB_PASS"
|
||||
envDBName string = "MF_MANAGER_DB"
|
||||
envPort string = "MF_MANAGER_PORT"
|
||||
envSecret string = "MF_MANAGER_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
Port string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
users := postgres.NewUserRepository(db)
|
||||
clients := postgres.NewClientRepository(db)
|
||||
channels := postgres.NewChannelRepository(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(cfg.Secret)
|
||||
|
||||
svc := manager.New(users, clients, channels, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "manager",
|
||||
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(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("Manager service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Manager service terminated: %s", err))
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"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/users"
|
||||
"github.com/mainflux/mainflux/users/api"
|
||||
grpcapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
httpapi "github.com/mainflux/mainflux/users/api/http"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/jwt"
|
||||
"github.com/mainflux/mainflux/users/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defSecret = "users"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
envDBPort = "MF_USERS_DB_PORT"
|
||||
envDBUser = "MF_USERS_DB_USER"
|
||||
envDBPass = "MF_USERS_DB_PASS"
|
||||
envDBName = "MF_USERS_DB"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
defer db.Close()
|
||||
|
||||
svc := newService(db, cfg.Secret, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Users service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func newService(db *sql.DB, secret string, logger log.Logger) users.Service {
|
||||
repo := postgres.New(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
|
||||
svc := users.New(repo, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "users",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "users",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc users.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Users HTTP service started, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc users.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.RegisterUsersServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Users gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
||||
+15
-7
@@ -9,33 +9,34 @@ 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"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
adapter "github.com/mainflux/mainflux/ws"
|
||||
"github.com/mainflux/mainflux/ws/api"
|
||||
"github.com/mainflux/mainflux/ws/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "8180"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defManagerURL = "http://localhost:8180"
|
||||
defClientsURL = "localhost:8181"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envManagerURL = "MF_MANAGER_URL"
|
||||
envClientsURL = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
@@ -49,6 +50,14 @@ func main() {
|
||||
}
|
||||
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)
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
@@ -72,9 +81,8 @@ func main() {
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc, logger))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
||||
+19
-12
@@ -9,9 +9,11 @@ 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 |
|
||||
|-----------------------|-------------------|-----------------------|
|
||||
| COAP_ADAPTER_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| 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
|
||||
|
||||
@@ -25,28 +27,33 @@ services:
|
||||
image: mainflux/coap-adapter:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:5683
|
||||
- [host machine port]:[configured port]
|
||||
environment:
|
||||
COAP_ADAPTER_NATS_URL: [NATS instance URL]
|
||||
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/cmd/coap
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the app; make sure to set the proper GOOS value
|
||||
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
|
||||
# compile the http
|
||||
make coap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
COAP_ADAPTER_NATS_URL=[NATS instance URL] app
|
||||
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-coap
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
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.
|
||||
|
||||
+139
-40
@@ -1,61 +1,160 @@
|
||||
package adapter
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"errors"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const protocol string = "coap"
|
||||
const (
|
||||
responseBackoffMultiplier = 1.5
|
||||
|
||||
type Observer struct {
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
message *coap.Message
|
||||
// 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)
|
||||
}
|
||||
|
||||
type CoAPAdapter struct {
|
||||
obsMap map[string][]Observer
|
||||
logger *zap.Logger
|
||||
pub mainflux.MessagePublisher
|
||||
var _ Service = (*adapterService)(nil)
|
||||
|
||||
type adapterService struct {
|
||||
pubsub nats.Service
|
||||
subs map[string]nats.Channel
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewCoAPAdapter creates new CoAP adapter struct
|
||||
func NewCoAPAdapter(logger *zap.Logger, pub mainflux.MessagePublisher) *CoAPAdapter {
|
||||
ca := &CoAPAdapter{
|
||||
logger: logger,
|
||||
pub: pub,
|
||||
obsMap: make(map[string][]Observer),
|
||||
// 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{},
|
||||
}
|
||||
|
||||
return ca
|
||||
}
|
||||
|
||||
// Serve function starts CoAP server
|
||||
func (ca *CoAPAdapter) Serve(addr string) error {
|
||||
ca.logger.Info("Starting CoAP server", zap.String("address", addr))
|
||||
return coap.ListenAndServe("udp", addr, ca.COAPServer())
|
||||
func (svc *adapterService) get(clientID string) (nats.Channel, bool) {
|
||||
svc.mu.Lock()
|
||||
obs, ok := svc.subs[clientID]
|
||||
svc.mu.Unlock()
|
||||
return obs, ok
|
||||
}
|
||||
|
||||
// BridgeHandler functions is a handler for messages received via NATS
|
||||
func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
|
||||
log.Printf("Received a message: %s\n", string(nm.Data))
|
||||
func (svc *adapterService) put(clientID string, obs nats.Channel) {
|
||||
svc.mu.Lock()
|
||||
svc.subs[clientID] = obs
|
||||
svc.mu.Unlock()
|
||||
}
|
||||
|
||||
// And write it into the database
|
||||
m := mainflux.RawMessage{}
|
||||
if len(nm.Data) > 0 {
|
||||
if err := json.Unmarshal(nm.Data, &m); err != nil {
|
||||
log.Println("Can not decode adapter msg")
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Calling obsTransmit()")
|
||||
log.Println(m.Publisher, m.Protocol, m.Channel, m.Payload)
|
||||
ca.obsTransmit(m)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -0,0 +1,66 @@
|
||||
// +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)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// +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)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
coap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
|
||||
log.Printf("Got message in sendMessage: path=%q: %#v from %v", m.Path(), m, a)
|
||||
var res *coap.Message
|
||||
if m.IsConfirmable() {
|
||||
res = &coap.Message{
|
||||
Type: coap.Acknowledgement,
|
||||
Code: coap.Content,
|
||||
MessageID: m.MessageID,
|
||||
Token: m.Token,
|
||||
Payload: []byte(""),
|
||||
}
|
||||
res.SetOption(coap.ContentFormat, coap.AppJSON)
|
||||
}
|
||||
|
||||
if len(m.Payload) == 0 {
|
||||
if m.IsConfirmable() {
|
||||
res.Code = coap.BadRequest
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Channel ID
|
||||
cid := mux.Var(m, "channel_id")
|
||||
|
||||
n := mainflux.RawMessage{
|
||||
Channel: cid,
|
||||
Publisher: "",
|
||||
Protocol: protocol,
|
||||
Payload: m.Payload,
|
||||
}
|
||||
|
||||
if err := ca.pub.Publish(n); err != nil {
|
||||
if m.IsConfirmable() {
|
||||
res.Code = coap.InternalServerError
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
if m.IsConfirmable() {
|
||||
res.Code = coap.Changed
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (ca *CoAPAdapter) registerObserver(o Observer, cid string) {
|
||||
found := false
|
||||
for _, v := range ca.obsMap[cid] {
|
||||
if v.addr == o.addr && bytes.Compare(v.message.Token, o.message.Token) == 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Println("Register " + cid)
|
||||
log.Printf("o.message = %v", o.message)
|
||||
ca.obsMap[cid] = append(ca.obsMap[cid], o)
|
||||
}
|
||||
}
|
||||
|
||||
func (ca *CoAPAdapter) deregisterObserver(o Observer, cid string) {
|
||||
for k, v := range ca.obsMap[cid] {
|
||||
if bytes.Compare(v.message.Token, o.message.Token) == 0 {
|
||||
// Observer found, remove it from array
|
||||
log.Println("Deregister " + cid)
|
||||
ca.obsMap[cid] = append((ca.obsMap[cid])[:k], (ca.obsMap[cid])[k+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ca *CoAPAdapter) observeMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
|
||||
log.Printf("Got message in observeMessage: path=%q: %#v from %v", m.Path(), m, a)
|
||||
var res *coap.Message
|
||||
|
||||
if m.IsConfirmable() {
|
||||
res = &coap.Message{
|
||||
Type: coap.Acknowledgement,
|
||||
Code: coap.Content,
|
||||
MessageID: m.MessageID,
|
||||
Token: m.Token,
|
||||
Payload: []byte(""),
|
||||
}
|
||||
res.SetOption(coap.ContentFormat, coap.AppJSON)
|
||||
}
|
||||
|
||||
// Channel ID
|
||||
cid := mux.Var(m, "channel_id")
|
||||
|
||||
// Observer
|
||||
o := Observer{
|
||||
conn: l,
|
||||
addr: a,
|
||||
message: m,
|
||||
}
|
||||
|
||||
if m.Option(coap.Observe) == nil {
|
||||
if m.IsConfirmable() {
|
||||
res.Code = coap.BadRequest
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
if value, ok := m.Option(coap.Observe).(uint32); ok && value == 0 {
|
||||
ca.registerObserver(o, cid)
|
||||
} else {
|
||||
ca.deregisterObserver(o, cid)
|
||||
}
|
||||
|
||||
if m.IsConfirmable() {
|
||||
res.Code = coap.Valid
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (ca *CoAPAdapter) obsTransmit(n mainflux.RawMessage) {
|
||||
for _, v := range ca.obsMap[n.Channel] {
|
||||
msg := *(v.message)
|
||||
msg.Payload = n.Payload
|
||||
|
||||
log.Printf("ca.obsMap[cid] = %v", v)
|
||||
log.Printf("msg = %v", msg)
|
||||
|
||||
msg.SetOption(coap.ContentFormat, coap.AppJSON)
|
||||
msg.SetOption(coap.LocationPath, msg.Path())
|
||||
|
||||
log.Printf("Transmitting %v", msg)
|
||||
err := coap.Transmit(v.conn, v.addr, msg)
|
||||
if err != nil {
|
||||
log.Printf("Error on transmitter, stopping: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Package nats contains NATS-specific message repository implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "src.coap"
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// NewMessagePublisher instantiates NATS message publisher.
|
||||
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
return &natsPublisher{nc}
|
||||
}
|
||||
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pub.nc.Publish(topic, data)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/dereulenspiegel/coap-mux"
|
||||
"github.com/dustin/go-coap"
|
||||
)
|
||||
|
||||
func notFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
|
||||
log.Printf("Got message in notFoundHandler: path=%q: %#v from %v", m.Path(), m, a)
|
||||
if m.IsConfirmable() {
|
||||
return &coap.Message{
|
||||
Type: coap.Acknowledgement,
|
||||
Code: coap.NotFound,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ca *CoAPAdapter) COAPServer() *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/channels/{channel_id}/messages", coap.FuncHandler(ca.sendMessage)).Methods(coap.POST)
|
||||
r.Handle("/channels/{channel_id}/messages", coap.FuncHandler(ca.observeMessage)).Methods(coap.GET)
|
||||
|
||||
r.NotFoundHandler = coap.FuncHandler(notFoundHandler)
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "mainflux-ui"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"project": "src/tsconfig.app.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,58 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
/.vscode
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
|
||||
#System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#json-server database
|
||||
db.json
|
||||
db.*
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Mainflux UI Dashboad
|
||||
|
||||
Mainflux UI, dashboard for [Mainflux](https://github.com/mainflux/mainflux) Industrial IoT Messaging and Device Management Server.
|
||||
|
||||
> **N.B.** Mainflux UI service is WIP and not suitable for deployment at this moment. You are welcome to contribute and improve it.
|
||||
> ## Development
|
||||
>- Follow angular-cli [documentation](https://github.com/angular/angular-cli)
|
||||
>- Follow [official angular style guide](https://angular.io/styleguide)
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need the following software installed to get started.
|
||||
|
||||
- [Node](https://nodejs.org/en/) 6 or higher, we recommend current LTS version, together with NPM 3 or higher.
|
||||
- [Angular-cli](https://github.com/angular/angular-cli) Newest version with Webpack integration
|
||||
- - Depending on how Node is configured on your machine, you may need to run installation command with `sudo`
|
||||
- [Git](http://git-scm.com/downloads): Use the installer for your OS.
|
||||
- Windows users can also try [Git for Windows](http://git-for-windows.github.io/).
|
||||
- For local Development with [Mainflux composition](https://github.com/mainflux/mainflux) running locally, [Chrome extension for Cross origin](https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?utm_source=chrome-app-launcher-info-dialog) is required. Because composition is running on different port then our Angular app, we have cross origin.
|
||||
|
||||
## Configuration
|
||||
|
||||
Change into the directory.
|
||||
|
||||
```bash
|
||||
cd dashflux
|
||||
```
|
||||
|
||||
Install the dependencies. If you're running Mac OS or Linux, you may need to run `sudo npm install` instead, depending on how your machine is configured.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Set appropriate endpoint URLs in **environment.ts** (for local development will probably be 0.0.0.0:<_port_>) or **environment.prod.ts** for production.
|
||||
|
||||
To start the server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
This will run and assemble our app.
|
||||
**Now go to `localhost:4200` in your browser to see it in action.**
|
||||
|
||||
## Deployment
|
||||
|
||||
Dashflux is distributed as Docker container. We use nginx to serve dashflux from docker container, supporting environments using docker multi-stage builds.
|
||||
Dashflux docker image is available on [Dockerhub mainflux/dashflux](https://hub.docker.com/r/mainflux/dashflux/)
|
||||
|
||||
If you want to build image locally, you can build image using the **development** environment:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t dashflux:dev --build-arg env=dev .
|
||||
```
|
||||
|
||||
Build image using the **production** environment:
|
||||
|
||||
```bash
|
||||
docker build -t dashflux:prod -f ./docker/Dockerfile .
|
||||
```
|
||||
|
||||
**Note:** before running *docker build* command, please make sure appropriate endpoint URLs in *environment.ts* or *environment.prod.ts* are set up.
|
||||
|
||||
You can test image running
|
||||
|
||||
```bash
|
||||
docker run -p 80:80 dashflux:dev
|
||||
```
|
||||
|
||||
This will run dashflux in docker container.
|
||||
|
||||
Now go to `http://localhost` in your browser to see it in action.
|
||||
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
@@ -0,0 +1,13 @@
|
||||
# Stage 0, based on Node.js, to build and compile Angular
|
||||
FROM node:8.6 as node
|
||||
WORKDIR /app
|
||||
COPY package.json /app/
|
||||
RUN npm install
|
||||
COPY ./ /app/
|
||||
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
|
||||
COPY --from=node /app/dist/ /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
@@ -0,0 +1,8 @@
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('mainflux-ui App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to app!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadless'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
Generated
+10394
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "mainflux-ui",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache License, version 2.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy-config.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.2.4",
|
||||
"@angular/common": "^5.2.4",
|
||||
"@angular/compiler": "^5.2.4",
|
||||
"@angular/core": "^5.2.4",
|
||||
"@angular/flex-layout": "2.0.0-beta.10-4905443",
|
||||
"@angular/forms": "^5.2.4",
|
||||
"@angular/http": "^5.2.4",
|
||||
"@angular/platform-browser": "^5.2.4",
|
||||
"@angular/platform-browser-dynamic": "^5.2.4",
|
||||
"@angular/router": "^5.2.4",
|
||||
"core-js": "^2.4.1",
|
||||
"mobx": "^3.5.1",
|
||||
"mobx-angular": "^2.1.1",
|
||||
"ngx-auth": "^2.2.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"zone.js": "^0.8.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cdk": "^5.2.0",
|
||||
"@angular/cli": "^1.7.3",
|
||||
"@angular/compiler-cli": "^5.2.4",
|
||||
"@angular/language-service": "^5.2.4",
|
||||
"@angular/material": "^5.2.0",
|
||||
"@types/jasmine": "~2.5.53",
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "^6.0.100",
|
||||
"codelyzer": "~3.2.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jasmine-core": "~2.5.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.2.0",
|
||||
"tslint": "~5.7.0",
|
||||
"typescript": "~2.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8180",
|
||||
"secure":false,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Route, RouterModule } from '@angular/router';
|
||||
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';
|
||||
|
||||
const routes: Route[] = [
|
||||
{ path: '', redirectTo: 'clients', pathMatch: 'full'},
|
||||
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'clients', component: ClientsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -0,0 +1,15 @@
|
||||
<div *mobxAutorun>
|
||||
<div class="loading" *ngIf="uiStore.loading" fxLayoutAlign="center center">
|
||||
<mat-progress-spinner [mode]="'indeterminate'"></mat-progress-spinner>
|
||||
</div>
|
||||
<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]="['/channels']" mat-button>Channels</a>
|
||||
<a (click)="logout()" mat-button>Logout</a>
|
||||
</ng-container>
|
||||
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
.loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { AppComponent } from './app.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 { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { reaction } from 'mobx';
|
||||
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
public uiStore: UiStore,
|
||||
public authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
reaction(() => this.authStore.authError, (authError) => {
|
||||
if (authError) {
|
||||
this.snackBar.open(authError, '', {
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authStore.logout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import './rxjs-extensions.ts';
|
||||
import 'hammerjs';
|
||||
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MobxAngularModule } from 'mobx-angular';
|
||||
import { AUTH_SERVICE, AuthModule, PROTECTED_FALLBACK_PAGE_URI, PUBLIC_FALLBACK_PAGE_URI } from 'ngx-auth';
|
||||
|
||||
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 { 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 { 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 { MockAuthService } from './core/services/mock-auth.service';
|
||||
import { MockChannelsService } from './core/services/mock-channels.service';
|
||||
import { MockClientsService } from './core/services/mock-clients.service';
|
||||
import { ChannelsStore } from './core/store/channels.store';
|
||||
import { ClientsStore } from './core/store/clients.store';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
import { UnauthorizedInterceptor } from './core/services/auth/unauthorized.interceptor';
|
||||
|
||||
export function factory(authenticationService: AuthenticationService) {
|
||||
return authenticationService;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ClientsComponent,
|
||||
ChannelsComponent,
|
||||
SignupComponent,
|
||||
LoginComponent,
|
||||
AddClientDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AuthModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
ReactiveFormsModule,
|
||||
MobxAngularModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
ClientsStore,
|
||||
ChannelsStore,
|
||||
AuthStore,
|
||||
MockAuthService,
|
||||
MockClientsService,
|
||||
MockChannelsService,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
{ provide: PROTECTED_FALLBACK_PAGE_URI, useValue: '/' },
|
||||
{ provide: PUBLIC_FALLBACK_PAGE_URI, useValue: '/login' },
|
||||
{
|
||||
provide: AUTH_SERVICE,
|
||||
deps: [AuthenticationService],
|
||||
useFactory: factory
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [
|
||||
AddClientDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
ConfirmationDialogComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
|
||||
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Login</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" class="emailInput" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%">
|
||||
<input type="password" class="passwordInput" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="loginForm.get('password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="loginForm.invalid" mat-raised-button color="primary" class="loginButton">Login</button>
|
||||
<button mat-button (click)="signup()" class="signupButton">Sign Up with Email</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { LoginComponent } from './login.component';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call the store goToSignup when clicked on Sign Up with Email', inject([UiStore], (store: UiStore) => {
|
||||
const signupButton = fixture.debugElement.nativeElement.querySelector('.signupButton');
|
||||
const signupSpy = spyOn(store, 'goToSignup').and.stub();
|
||||
|
||||
signupButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(signupSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
loginForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.loginForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required]],
|
||||
});
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authStore.login(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.uiStore.goToSignup();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.loginForm.get('email').value,
|
||||
password: this.loginForm.get('password').value
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
|
||||
<form fxLayout="column" [formGroup]="signupForm" (ngSubmit)="signup()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Signup</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Repeat password" formControlName="repeatPassword" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.repeatPassword').errors?.required">
|
||||
Repeat the password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="signupForm.invalid" mat-raised-button color="primary" class="large-button">Signup</button>
|
||||
<a mat-button (click)="login()">Login</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { SignupComponent } from './signup.component';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('SignupComponent', () => {
|
||||
let component: SignupComponent;
|
||||
let fixture: ComponentFixture<SignupComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SignupComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SignupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signup',
|
||||
templateUrl: './signup.component.html',
|
||||
styleUrls: ['./signup.component.scss']
|
||||
})
|
||||
export class SignupComponent implements OnInit {
|
||||
signupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.signupForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
passwords: this.fb.group({
|
||||
password: ['', [Validators.required]],
|
||||
repeatPassword: ['', [Validators.required]]
|
||||
}, { validator: this.comparePasswords })
|
||||
});
|
||||
}
|
||||
|
||||
comparePasswords(c: AbstractControl): { [key: string]: boolean } {
|
||||
const pass = c.get('password');
|
||||
const repeatPassword = c.get('repeatPassword');
|
||||
|
||||
if (pass.value !== repeatPassword.value) {
|
||||
return { 'match': true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.authStore.signup(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
login() {
|
||||
this.uiStore.goToLogin();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.signupForm.get('email').value,
|
||||
password: this.signupForm.get('passwords.password').value
|
||||
};
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<form [formGroup]="addChannelForm" (ngSubmit)="onAddChannel()">
|
||||
<h1 *ngIf="!editMode" mat-dialog-title>Add channel</h1>
|
||||
<h1 *ngIf="editMode" mat-dialog-title>Edit channel</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field>
|
||||
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
|
||||
<mat-error *ngIf="addChannelForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</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>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<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>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('AddChannelDialogComponent', () => {
|
||||
let component: AddChannelDialogComponent;
|
||||
let fixture: ComponentFixture<AddChannelDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddChannelDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
ClientsStore,
|
||||
UiStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddChannelDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-channel-dialog',
|
||||
templateUrl: './add-channel-dialog.component.html',
|
||||
styleUrls: ['./add-channel-dialog.component.scss']
|
||||
})
|
||||
export class AddChannelDialogComponent implements OnInit {
|
||||
addChannelForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddChannelDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Channel,
|
||||
public clientsStore: ClientsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addChannelForm = this.fb.group(
|
||||
{
|
||||
id: [''],
|
||||
name: [''],
|
||||
connected: [[]]
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.editMode = true;
|
||||
this.addChannelForm.patchValue(toJS(this.data));
|
||||
} else {
|
||||
this.editMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
onAddChannel() {
|
||||
const channel = this.addChannelForm.value;
|
||||
this.submit.emit(channel);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
compareFunction(obj1: Client, obj2: Client) {
|
||||
return obj1.id === obj2.id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="channelsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="channelsStore.channels?.length > 0">
|
||||
<ng-container *ngSwitchCase="true">
|
||||
<!-- List -->
|
||||
<div class="channel-list-container">
|
||||
<mat-table #table [dataSource]="channelsStore.channels.toJS()" matSort class="mat-cell">
|
||||
<!-- Columns -->
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell fxFlex="32%" *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
|
||||
<mat-cell fxFlex="32%" *matCellDef="let row" >{{row.id}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell fxFlex="30%" *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell fxFlex="30%" *matCellDef="let row"> {{row.name}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="connected">
|
||||
<mat-header-cell fxFlex="30%" *matHeaderCellDef mat-sort-header>Connected</mat-header-cell>
|
||||
<mat-cell fxFlex="30%" *matCellDef="let row">
|
||||
<li *ngFor="let item of row.connected"> {{item.id}} </li>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Actions -->
|
||||
<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)="editChannel(row)">
|
||||
<mat-icon aria-label="Delete">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="deleteChannel(row)">
|
||||
<mat-icon aria-label="Delete">delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
<h3>It looks like you don't have any channels in your account.</h3>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button mat-mini-fab class="addButton" (click)="addChannel()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
@@ -0,0 +1,10 @@
|
||||
.addButton {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.channel-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { ChannelsComponent } from './channels.component';
|
||||
import { ClientsStore } from '../../core/store/clients.store';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
describe('ChannelsComponent', () => {
|
||||
let component: ChannelsComponent;
|
||||
let fixture: ComponentFixture<ChannelsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ChannelsComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ClientsStore,
|
||||
useClass: class {
|
||||
getClients = jasmine.createSpy('getClients');
|
||||
}
|
||||
},
|
||||
UiStore,
|
||||
{
|
||||
provide: ChannelsStore,
|
||||
useClass: class {
|
||||
getChannels = jasmine.createSpy('getChannels');
|
||||
}
|
||||
},
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChannelsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
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 { Channel } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { AddChannelDialogComponent } from './add-channel-dialog/add-channel-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channels',
|
||||
templateUrl: './channels.component.html',
|
||||
styleUrls: ['./channels.component.scss'],
|
||||
})
|
||||
export class ChannelsComponent implements OnInit {
|
||||
channels: Observable<Channel[]>;
|
||||
displayedColumns = ['id', 'name', 'connected', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public clientsStore: ClientsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.channelsStore.getChannels();
|
||||
this.clientsStore.getClients();
|
||||
}
|
||||
|
||||
addChannel() {
|
||||
const dialogRef = this.dialog.open(AddChannelDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
|
||||
this.channelsStore.addChannel(channel);
|
||||
});
|
||||
}
|
||||
|
||||
editChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(AddChannelDialogComponent, {
|
||||
data: channel
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedChannel: Channel) => {
|
||||
this.channelsStore.editChannel(editedChannel);
|
||||
});
|
||||
}
|
||||
|
||||
deleteChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the channel?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.channelsStore.deleteChannel(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<form [formGroup]="addClientForm" (ngSubmit)="onAddClient()">
|
||||
<h1 mat-dialog-title>Add client</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Client type" formControlName="type">
|
||||
<mat-option value="app">
|
||||
App
|
||||
</mat-option>
|
||||
<mat-option value="device">
|
||||
Device
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="addClientForm.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">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea formControlName="payload" matInput placeholder="Payload" matTextareaAutosize matAutosizeMinRows="2"
|
||||
matAutosizeMaxRows="5"></textarea>
|
||||
</mat-form-field>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user