Compare commits

..

50 Commits

Author SHA1 Message Date
Dušan Borovčanin 50e14f0c65 NOISSUE - Add CoAP adapter version endpoint (#277) 2018-05-14 13:21:49 +02:00
Dušan Borovčanin d2bce84c06 NOISSUE - Update Docker compose (#274) 2018-05-14 13:11:29 +02:00
Aleksandar Novaković 89aa9603ec NOISSUE - Replace manager with users and clients services (#276) 2018-05-14 12:18:01 +02:00
Aleksandar Novaković 2ae581368f NOISSUE - Add WebSocket and CoAP services to kubernetes config (#275) 2018-05-14 12:13:04 +02:00
Aleksandar Novaković 99a3ec5d2d NOISSUE - Fix kubernetes configuration (#273) 2018-05-11 16:41:25 +02:00
Dušan Borovčanin 89a0fa3e9c NOISSUE - Handle authorization errors other than unauthorize (#264) 2018-05-11 16:37:32 +02:00
Dejan Mijić 1bc4dc9575 NOISSUE - Add MQTT build steps into Makefile (#267) 2018-05-11 01:13:33 +02:00
Dejan Mijić bca3d0774f MF-201 - Replace ORM with SQL (#265) 2018-05-11 01:00:10 +02:00
Aleksandar Novaković 816c172823 MF-164 - Split manager service (#266) 2018-05-10 23:53:25 +02:00
Dušan Borovčanin 6a361209c8 NOISSUE - Automate Dashflux container deployment (#261) 2018-05-10 10:39:48 +02:00
Dušan Borovčanin 27e1be23f5 NOISSUE - Integrate MQTT adapter to monorepo (#260) 2018-05-09 14:22:03 +02:00
Dušan Borovčanin c9bf245010 NOISSUE - Update CoAP docs (#257) 2018-05-09 12:56:20 +02:00
Dušan Borovčanin ff30957614 MF-244 - Integrate Dashflux (#258) 2018-05-09 12:32:49 +02:00
Dušan Borovčanin db6165aa6b MF-168 - Refactor CoAP adapter (#186) 2018-05-08 16:00:25 +02:00
Aleksandar Novaković c088e664c3 NOISSUE - Fix run and compile commands in README files (#249) 2018-05-02 09:41:21 +02:00
Dejan Mijić d499068e43 Update protobuf to 1.1.0 (#251) 2018-05-02 08:49:39 +02:00
Dušan Borovčanin 38d4c3fcea MF-136 - Setup staging and test environments (#226) 2018-04-30 13:32:38 +02:00
Aleksandar Novaković aea7db14b7 NOISSUE - Raise code coverage in ws adapter (#242)
* Fix failed subscription handling in ws adapter

Fix unsubscribing bug in ws adapter.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add subscription fail and publish fail test cases

Update mock implementation to receive publish error in order to
support these test cases.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update mainflux version to 0.2.3

Update project version and load tests version to 0.2.3.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update version endpoint response format

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-24 13:56:13 +02:00
Aleksandar Novaković 58f3c735a0 NOISSUE - Fix Content-Type in examples (#243)
* Update content-type header in load tests

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Remove charset from content type header in documentation

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-24 13:42:36 +02:00
Dušan Borovčanin 789963447a Add service name to version response (#241)
Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-24 13:39:15 +02:00
Dušan Borovčanin 2dc7f3eea1 NOISSUE - Raise endpoint test coverage (#240)
Ramove unnecessary checks from decodeList.
Cover some edge cases and fix decodeList to always return same error.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-23 15:17:30 +02:00
Aleksandar Novaković 62297fedec MF-134 - Evaluate system's performance (#225)
* Add initial load tests

Add initial load tests for client creation and message publishing.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Make load tests executable as stand-alone application

Move code from test to main. Make code runnable with sbt run command.
Remove unnecessary config files.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add native packager support

Add native packager plugin. Update sbt config to support native
packager. Update paths in Engine.scala.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Move files back to test folder and remove native packager support

Remove nativa packager plugin. Add gatling plugin and move files to
test folder where they belong. Read vars from JAVA_OPTS instead of
environment variables.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add readme file for load tests

Add readme file for load tests with usage instructions.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add number of requests per second as test parameter

Add number of requests per second as parameter. Update read me according
to this addition.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add load test section in docs

Create documentation skeleton for load tests.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Reformat logger config file

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update documentation skeleton

Move results section to scenarios. Move test environment to intro.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Align test version with mainflux version

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update mainflux version to 0.2.2

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-23 15:13:43 +02:00
Dejan Mijić f449f8b9c8 NOISSUE - Fix Content-Type header check (#238)
* Remove charset from required content type

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Bump version to 0.2.1

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Move constant to transport.go

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-22 16:48:06 +02:00
Dejan Mijić 544ba57850 NOISSUE - Ignore logging & metrics for code coverage (#233)
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-19 13:40:22 +02:00
Dejan Mijić e858e86ed9 NOISSUE - Fix paging (#232)
* Fix docs errors

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Fix edge case for limit check

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Explain limit value

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-18 23:06:11 +02:00
Dušan Borovčanin 84679ed42a MF-200 - Enable pagination of result sets (#227)
* Add pagination to clients and channels endpoints

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Refactor code

Change method signature and rename Bulk methods back to All.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Rename transport_test.go to endpoint_test.go

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix manager tests to support pagination

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Add default offset and limit support

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Update docs

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Update tests to support pagination

- Move maxLimitSize checking to request validation.
- Add tests to support pagination.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix handling query params for pagination

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix empty result set

Return empty results if invalid offset and limit is passed to channel and client repository.
Update tests accordingly.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Update manager API docs

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Fix response to invalid limit query param

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Remove offset and limmit checks in repository methods

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-18 22:36:24 +02:00
Aleksandar Novaković b0610f034c Fix http publish example in docs (#231)
Fix http publish example in docs. Fix docker-compose example in docs
and README.md.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-18 16:49:10 +02:00
Dejan Mijić cb9c0ee32a Specify targets for releasing docker containers (#230)
New targets will allow Makefile to be used as primary option for
releasing docker containers, both as 'latest' and versioned.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-18 14:13:38 +02:00
Aleksandar Novaković 3a5f4395e4 MF-171 - Extract websocket adapter as separate service (#188)
* Add websocket adapter

Add websocket adapter with basic logging and metrics middleware.
Add publish and subscribe to NATS subjects using websocket.
Add websocket handshake authorization over mainflux manager.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add test for websocket publish

Add test for websocket adapter's publish method. Add dependecy
injected logger to adapter. Remove unnecessary manager client
dependency from adapter.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add websocket library in dependencies

Add gorilla/websocket dependency in dep toml and lock file.
Add dependency in vendor dir.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add direct websocket connection

Add direct websocket connection. While messages are still beeing
published over NATS, they are not received over src.ws subject
in WebSocket adapter. Instead messages are sent directly over
websocket connection.

Add swagger file for WebSocket handshake endpoint. Update ReadMe
to reference new swagger file.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactore websocket adapter code

Extract listen part from handshake. Update WebSocket adapter code.
Fix subscribe to NATS subject, so that it is independent from other
adapters. Remove message base64 encoding from response..

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Remove connection management from websocket adapter

Align notion of channel with NATS topic. Remove connection
management from adapter logic. Add log messages to adapter's
transport layer.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update NATS subjects and add subscriber interface

Update NATS subject name to channel.<channel_id>. Add and implement
subscriber interface. Implement subscriber interface in adapter.
Update readme to use new make <service_name> command. Refactor code.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add basic tests for broadcast and subscribe

Improve mock implementation of NATS pubsub. Add multiple basic test
cases for broadcast and subscribe methods. Add logging for subscribe.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add listen method test

Add listen method test and refactor existing tests. Refactor listen
method in adapter.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Move broadcast method to message broker interface

Refactor broadcast method to receive send message callback and message
that needs to be sent. Update tests accordingly.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update pubsub API

Remove listen method from public API. Move listen call to subscribe
implementation. Update domain pubsub API in project root.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add connection error handlers to adapter API

Update publish and subscribe API to receive connection error handler.
Update tests accordingly. Handle NATS connection error.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update logs

Replace go-kit logger with custom mainflux logger. Update log messages
where needed..

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactor web socket adapter

Remove MessagePubSub interface. Remove unnecessary callbacks. Add
channels to web socket adapter implementation.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add API layer tests

Update existing tests and add API layer tests.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update docs with web socket related data

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactor web socket adapter

Update subscription struct, and refactor listen and broadcast methods.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add version and metrics endpoint to ws adapter

Update version to 0.2.0.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Fix race condition bug in ws adapter test

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Rename listen to broadcast and broadcast to listen

Switch names between listen and broadcast methods. Move channel
structure to service.go.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-18 13:09:01 +02:00
Aleksandar Novaković 36a00f3464 Update project documentation with example requests and responses (#220)
Update documentation with example requests and responses in accordance
with new API changes. Add detailed flow explanation. Update readme with
valid snippet.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-11 14:30:37 +02:00
Aleksandar Novaković 703d0543af Add HTTP adapter API test (#224)
Add HTTP adapter API tests. Fix Manager API tests.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-11 14:29:04 +02:00
Aleksandar Novaković fb3619645d Add channel Id validation (#223)
Add channel id validation and remove unnecessary error. Update
swagger doc accordingly.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-11 13:48:09 +02:00
Dejan Mijić 46a9cc086d Remove unhandled missing key error (#222)
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-11 11:44:15 +02:00
Dejan Mijić 367d3edbef Set JWT provider secret (#221)
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-10 17:30:13 +02:00
Dejan Mijić d5b913d432 MF-199 - Create documentation skeleton (#218)
* Copy basic docs contents to the main repo

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Include licensing and contributing info to docs

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Create documentation skeleton

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-04-08 23:03:12 +02:00
Aleksandar Novaković 2dace5564f MF-170 - Add manager API unit tests (#211)
* Fix connection request validation and EOF error handling

Fix validation of connection and client id in connection request.
Fix EOF error handling by returning HTTP status Bad Request.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add manager API tests and update swagger

Implement unit tests for every manager API endpoint. Update client
and connection mock implementations by switching to UUIDs. Update
swagger file with correct status codes.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add content type check and update documentation

Add content type check in implementation and update documentation
accordingly. Refactor tests and add empty content type test cases.
Add code coverage badge to readme.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-08 22:57:56 +02:00
Aleksandar Novaković 88b30626dd MF-174 - Add logger interface and go-kit logger implementation (#212)
* Add logger interface and go-kit logger implementation

Add wrapper logger interface and wrap go-kit logger in it. Add
tests for info, warn, error and log methods. Add log filtering by
level.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Refactor logger and replace go-kit logger with wrapper

Replace go-kit logger from services with logger wrapper. Refactor
code in logger wrapper. Remove unnecessary subpackage, methods and
log levels.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-04 10:15:23 +02:00
Drasko DRASKOVIC 301d855015 Add CA cert (#215)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-04-04 02:29:29 +02:00
Dušan Borovčanin 7a193829f4 MF-139 - Expose metrics from all services (#213)
* Expose metrics from normalizer service

Add service interface in order to expose method to be monitored.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Refactor normalizer

Add metrics to normalizer service without exporting its internals.

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-03 20:25:49 +02:00
Dejan Mijić 523716b090 Rename NATS subjects for HTTP & normalizer (#207)
* Change NATS subject name

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Change normalizer subscription name

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-22 23:59:15 +01:00
Dejan Mijić 36ca9015ff Handle wrong type unmarshalling (#206)
* Update the contributing guide

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Handle wrong type unmarshalling

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-22 23:53:43 +01:00
Drasko DRASKOVIC a6dfc26959 Simplify Makefile (#205)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-22 13:10:11 +01:00
Dušan Borovčanin 924f6f120a Fix blinker tests (#193)
* Fix blinker tests

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>

* Add protobuf generated files to .gitignore

Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-03-20 09:14:54 +01:00
Drasko DRASKOVIC 2c1c94af9b Fix #195 (#196)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-20 00:09:44 +01:00
Dejan Mijić f5fbd6f22a Add protobuf (#192)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-19 10:14:43 +01:00
Aleksandar Novaković 55bdc029c3 Add service startup message (#190)
* Add service startup message

Add startup message to manager, http, coap and normalizer services.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Remove startup message from CoAP adapter

Remove startup message from CoAP adapter. Remove overhead startup
message variable.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-03-18 13:54:54 +01:00
Dejan Mijić e42d422410 Fix golint warnings (#189)
* Fix goling warnings

Signed-off-by: Dejan Mijic <dejan@mainflux.com>

* Bump version to 0.1.2

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-16 14:06:12 +01:00
Nikola Marčetić bf5d1544b6 Fixed docker composition (#187)
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2018-03-16 13:52:51 +01:00
Max Peng 8876acbfec Fix docker compose failing start issue (#185)
* Fix docker compose failing start issue

* Delete unused code

* Fix nginx config issue
2018-03-16 09:59:13 +01:00
anovakovic01 250505700b Fix normalizer output queue (#184)
Fix normalizer output queue by changing subject to normalized.
Version updated from 0.1.0 to 0.1.1.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-03-15 14:23:20 +01:00
1119 changed files with 223459 additions and 32719 deletions
+38 -48
View File
@@ -1,7 +1,7 @@
# Contributing to Mainflux
The following is a set of guidelines for contributing to Mainflux and its libraries, which are
hosted in the [Mainflux Organization](https://github.com/mainflux) on GitHub.
hosted in the [Mainflux Organization](https://github.com/mainflux) on GitHub.
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
By participating, you are expected to uphold this code. Please report unacceptable behavior to
@@ -10,7 +10,7 @@ By participating, you are expected to uphold this code. Please report unacceptab
## Reporting issues
Reporting issues is a great way to contribute to the project. We always appreciate a well-written,
thorough bug reports.
thorough bug reports.
Prior to raising a new issue, check out [our issue
list](https://github.com/mainflux/mainflux/issues) to determine whether it already include the
@@ -36,63 +36,53 @@ refactoring code etc.), otherwise you risk spending a lot of time working on som
maintainers might not want to merge into the project.
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
following style guides:
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
- [Effective Go](https://golang.org/doc/effective_go.html)
- [Google's JavaScript styleguide](https://google.github.io/styleguide/jsguide.html)
To start contributing to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
clone your fork repository, and configure the remotes:
Adhering to the following process is the best way to get your work included in the project:
```
git clone https://github.com/<your-username>/mainflux.git
cd mainflux
git remote add upstream https://github.com/mainflux/mainflux.git
```
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure
the remotes:
If you cloned a while ago, get the latest changes from upstream:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/mainflux.git
```
git checkout master
git pull --rebase upstream master
```
# Navigate to the newly cloned directory
cd mainflux
Create a new topic branch from `master` using the naming convention `MF-[issue-number]`
to help us keep track of your contribution scope:
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/mainflux/mainflux.git
```
```
git checkout -b MF-[issue-number]
```
2. If you cloned a while ago, get the latest changes from upstream:
Commit your changes in logical chunks. When you are ready to commit, make sure
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
to group your commits into logical units of working before making them public.
```bash
git checkout master
git pull upstream master
```
Note that every commit you make must be signed. By signing off your work you indicate that you
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
3. Create a new topic branch from `master` using the naming convention `mainflux-[issue-num]` to
help us keep track of your contribution scope:
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
```bash
git checkout -b mainflux-[issue-num]
```
Locally merge (or rebase) the upstream development branch into your topic branch:
4. Commit your changes in logical chunks. When you are ready to commit, make sure to write a Good
Commit Message™. Consult the [https://github.com/erlang/otp/wiki/Writing-good-commit-messages](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
to group your commits into logical units of working before making them public.
```
git pull --rebase upstream master
```
Note that every commit you make must be signed. By signing off your work you indicate that you
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
Push your topic branch up to your fork:
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
```
git push origin MF-[issue-number]
```
5. Locally merge (or rebase) the upstream development branch into your topic branch:
```bash
git pull [--rebase] upstream master
```
6. Push your topic branch up to your fork:
```bash
git push origin mainflux-[issue-num]
```
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
and detailed description.
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
and detailed description.
+3
View File
@@ -1 +1,4 @@
build
*.pb.go
site/
Generated
+146 -103
View File
@@ -25,20 +25,20 @@
[[projects]]
name = "github.com/asaskevich/govalidator"
packages = ["."]
revision = "73945b6115bfbbcc57d89b7316e28109364124e1"
version = "v7"
revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
version = "v9"
[[projects]]
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"
@@ -67,40 +67,8 @@
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
version = "v3.1.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 = "4d9beb4607404e4d756052aca7041517788f7e75"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
name = "github.com/docker/go-connections"
@@ -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 = "1.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,24 +128,21 @@
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/jinzhu/gorm"
packages = [
".",
"dialects/postgres"
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp"
]
revision = "58e34726dfc069b558038efbaa25555f182d1f7a"
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/jinzhu/inflection"
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
branch = "master"
@@ -195,10 +155,9 @@
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid"
]
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
@@ -213,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"
@@ -246,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 = ["."]
@@ -281,7 +271,7 @@
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
[[projects]]
branch = "master"
@@ -292,7 +282,16 @@
"nfs",
"xfs"
]
revision = "282c8707aa210456a825798969cc27edda34992a"
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
[[projects]]
branch = "master"
name = "github.com/rubenv/sql-migrate"
packages = [
".",
"sqlparse"
]
revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906"
[[projects]]
name = "github.com/satori/go.uuid"
@@ -303,8 +302,8 @@
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
version = "v1.0.4"
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
name = "github.com/sony/gobreaker"
@@ -321,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"
@@ -357,16 +331,21 @@
"blowfish",
"ssh/terminal"
]
revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp"
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/timeseries",
"trace"
]
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
[[projects]]
branch = "master"
@@ -375,17 +354,81 @@
"unix",
"windows"
]
revision = "88d2dcc510266da9f7f8c7f34e1940716cab5f5c"
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 = "9df3ffee2bc61a6ce786baa7570eb61d8275197ea36560cf0cfd7cc0ea53af96"
inputs-digest = "cce4c6cd08b6213615afc135d8e7d4f255d4ee32b42062e1f3208de246366418"
solver-name = "gps-cdcl"
solver-version = 1
+36 -20
View File
@@ -1,14 +1,22 @@
[[constraint]]
name = "github.com/asaskevich/govalidator"
version = "7.0.0"
version = "9.0.0"
[[constraint]]
branch = "master"
name = "github.com/cisco/senml"
[[constraint]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.0.0"
version = "3.2.0"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-coap"
[[constraint]]
name = "github.com/go-kit/kit"
@@ -19,16 +27,20 @@
version = "1.2.0"
[[constraint]]
branch = "master"
name = "github.com/golang/protobuf"
version = "1.1.0"
[[constraint]]
name = "github.com/jinzhu/gorm"
version = "1.9.1"
[[constraint]]
branch = "master"
name = "github.com/lib/pq"
[[constraint]]
name = "github.com/nats-io/go-nats"
version = "1.3.0"
[[constraint]]
name = "gopkg.in/ory-am/dockertest.v3"
version = "3.1.6"
version = "1.4.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
@@ -42,22 +54,26 @@
name = "github.com/sony/gobreaker"
version = "0.3.0"
[[constraint]]
name = "go.uber.org/zap"
version = "1.7.0"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-coap"
[[constraint]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "gopkg.in/ory-am/dockertest.v3"
version = "3.1.6"
[[constraint]]
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 -1
View File
@@ -176,7 +176,7 @@
END OF TERMS AND CONDITIONS
Copyright 2015 Mainflux
Copyright 2015-2018 Mainflux
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+36 -28
View File
@@ -1,29 +1,20 @@
BUILD_DIR=build
SERVICES=manager http normalizer coap
DOCKERS=$(addprefix docker_,$(SERVICES))
all: $(SERVICES)
.PHONY: all $(SERVICES) docker
BUILD_DIR = build
SERVICES = users clients http normalizer ws coap
DOCKERS = $(addprefix docker_,$(SERVICES))
CGO_ENABLED ?= 0
GOOS ?= linux
define compile_service
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
endef
define make_docker
docker build --build-arg SVC_NAME=$(subst docker_,,$(1)) --tag=mainflux/$(subst docker_,,$(1)) -f docker/Dockerfile .
endef
manager:
$(call compile_service,$(@))
all: $(SERVICES)
http:
$(call compile_service,$(@))
normalizer:
$(call compile_service,$(@))
coap:
$(call compile_service,$(@))
.PHONY: all $(SERVICES) dockers latest release
clean:
rm -rf ${BUILD_DIR}
@@ -31,18 +22,35 @@ clean:
install:
cp ${BUILD_DIR}/* $(GOBIN)
# Docker
docker_manager:
proto:
protoc --go_out=plugins=grpc:. *.proto
$(SERVICES): proto
$(call compile_service,$(@))
$(DOCKERS):
$(call make_docker,$(@))
docker_http:
$(call make_docker,$(@))
dockers: $(DOCKERS)
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile mqtt
docker_normalizer:
$(call make_docker,$(@))
docker_coap:
$(call make_docker,$(@))
docker: $(DOCKERS)
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))
git checkout $(version)
$(MAKE) dockers
for svc in $(SERVICES); do \
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)
+5 -3
View File
@@ -2,6 +2,7 @@
[![build][ci-badge]][ci-url]
[![go report card][grc-badge]][grc-url]
[![coverage][cov-badge]][cov-url]
[![license][license]](LICENSE)
[![chat][gitter-badge]][gitter]
@@ -33,8 +34,7 @@ Before proceeding, install the following prerequisites:
Once everything is installed, execute the following commands from project root:
```bash
cd docker/
docker-compose up -d
docker-compose -f docker/docker-compose.yml up -d
```
## Contributing
@@ -51,7 +51,7 @@ Thank you for your interest in Mainflux and wish to contribute!
- [Gitter][gitter]
- [Twitter][twitter]
[banner]: https://github.com/mainflux/doc/blob/master/docs/img/gopherBanner.jpg
[banner]: https://github.com/mainflux/mainflux/blob/master/docs/img/gopherBanner.jpg
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
[ci-url]: https://semaphoreci.com/mainflux/mainflux
[docs]: http://mainflux.readthedocs.io
@@ -61,5 +61,7 @@ Thank you for your interest in Mainflux and wish to contribute!
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
[grc-badge]: https://goreportcard.com/badge/github.com/mainflux/mainflux
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
[cov-badge]: https://codecov.io/gh/mainflux/mainflux/branch/master/graph/badge.svg
[cov-url]: https://codecov.io/gh/mainflux/mainflux
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
[twitter]: https://twitter.com/mainflux
+13
View File
@@ -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
}
+81
View File
@@ -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
+49
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
// Package grpc contains implementation of clients service gRPC API.
package grpc
+22
View File
@@ -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
}
}
+87
View File
@@ -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()))
}
}
+18
View File
@@ -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
}
+6
View File
@@ -0,0 +1,6 @@
package grpc
type accessRes struct {
id string
err error
}
+59
View File
@@ -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")
}
}
+2
View File
@@ -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)
@@ -97,21 +67,21 @@ func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
return nil, err
}
clients, err := svc.ListClients(req.key)
clients, err := svc.ListClients(req.key, req.offset, req.limit)
if err != nil {
return nil, err
}
return listClientsRes{clients, len(clients)}, nil
return listClientsRes{clients}, nil
}
}
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)
@@ -187,21 +157,21 @@ func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
return nil, err
}
channels, err := svc.ListChannels(req.key)
channels, err := svc.ListChannels(req.key, req.offset, req.limit)
if err != nil {
return nil, err
}
return listChannelsRes{channels, len(channels)}, nil
return listChannelsRes{channels}, nil
}
}
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
}
}
+610
View File
@@ -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,29 +1,23 @@
package api
package http
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/clients"
)
const maxLimitSize = 100
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
@@ -31,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()
@@ -45,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()
@@ -62,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
@@ -76,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
@@ -98,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
@@ -110,35 +104,35 @@ func (req viewResourceReq) validate() error {
type listResourcesReq struct {
key string
size int
offset int
limit int
}
func (req listResourcesReq) validate() error {
func (req *listResourcesReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
return clients.ErrUnauthorizedAccess
}
if req.size > 0 && req.offset >= 0 {
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 = manager.Client{Type: "app"}
channel manager.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 {
@@ -179,22 +161,23 @@ func TestListResourcesReqValidation(t *testing.T) {
cases := map[string]struct {
key string
size int
offset int
limit int
err error
}{
"valid listing request": {key, value, value, nil},
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
"negative offset": {key, value, -value, manager.ErrMalformedEntity},
"zero size": {key, 0, value, manager.ErrMalformedEntity},
"negative size": {key, -value, 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 {
req := listResourcesReq{
key: tc.key,
size: tc.size,
offset: tc.offset,
limit: tc.limit,
}
err := req.validate()
+201
View File
@@ -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,39 +1,36 @@
package api
package http
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
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"
)
const contentType = "application/json"
var (
errUnsupportedContentType = errors.New("unsupported content type")
errInvalidQueryParams = errors.New("invalid query params")
)
// 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,
@@ -118,45 +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) {
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) {
var client manager.Client
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var client clients.Client
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
return nil, err
}
@@ -170,7 +140,11 @@ func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, erro
}
func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error) {
var client manager.Client
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var client clients.Client
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
return nil, err
}
@@ -185,7 +159,11 @@ func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error)
}
func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, error) {
var channel manager.Channel
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var channel clients.Channel
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
return nil, err
}
@@ -199,7 +177,11 @@ func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, err
}
func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error) {
var channel manager.Channel
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var channel clients.Channel
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
return nil, err
}
@@ -223,10 +205,36 @@ func decodeView(_ context.Context, r *http.Request) (interface{}, error) {
}
func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errInvalidQueryParams
}
offset := 0
limit := 10
off, lmt := q["offset"], q["limit"]
if len(off) > 1 || len(lmt) > 1 {
return nil, errInvalidQueryParams
}
if len(off) == 1 {
offset, err = strconv.Atoi(off[0])
if err != nil {
return nil, errInvalidQueryParams
}
}
if len(lmt) == 1 {
limit, err = strconv.Atoi(lmt[0])
if err != nil {
return nil, errInvalidQueryParams
}
}
req := listResourcesReq{
key: r.Header.Get("Authorization"),
size: 10,
offset: 0,
offset: offset,
limit: limit,
}
return req, nil
@@ -235,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
@@ -245,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
}
}
@@ -264,20 +272,30 @@ 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)
case errInvalidQueryParams:
w.WriteHeader(http.StatusBadRequest)
case io.ErrUnexpectedEOF:
w.WriteHeader(http.StatusBadRequest)
case io.EOF:
w.WriteHeader(http.StatusBadRequest)
default:
if _, ok := err.(*json.SyntaxError); ok {
switch err.(type) {
case *json.SyntaxError:
w.WriteHeader(http.StatusBadRequest)
return
case *json.UnmarshalTypeError:
w.WriteHeader(http.StatusBadRequest)
default:
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusInternalServerError)
}
}
+192
View File
@@ -0,0 +1,192 @@
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux/clients"
log "github.com/mainflux/mainflux/logger"
)
var _ clients.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc clients.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc clients.Service, logger log.Logger) clients.Service {
return &loggingMiddleware{logger, svc}
}
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 {
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.AddClient(key, client)
}
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 {
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.UpdateClient(key, client)
}
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 {
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.ViewClient(key, id)
}
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 {
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.ListClients(key, offset, limit)
}
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_client for key %s and client %s took %s to complete", key, 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.RemoveClient(key, id)
}
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 {
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.CreateChannel(key, channel)
}
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 {
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.UpdateChannel(key, channel)
}
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 {
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.ViewChannel(key, id)
}
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 {
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.ListChannels(key, offset, limit)
}
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_channel for key %s and channel %s took %s to complete", key, 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.RemoveChannel(key, id)
}
func (lm *loggingMiddleware) Connect(key, chanID, clientID string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method connect for key %s, channel %s, client %s took %s to complete", key, chanID, clientID, 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.Connect(key, chanID, clientID)
}
func (lm *loggingMiddleware) Disconnect(key, chanID, clientID string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method disconnect for key %s, channel %s, client %s took %s to complete", key, chanID, clientID, 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.Disconnect(key, chanID, clientID)
}
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))
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.CanAccess(key, id)
}
@@ -1,23 +1,25 @@
// +build !test
package api
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,
@@ -25,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())
@@ -52,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())
@@ -61,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())
@@ -70,13 +54,13 @@ func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client,
return ms.svc.ViewClient(key, id)
}
func (ms *metricsMiddleware) ListClients(key string) ([]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())
}(time.Now())
return ms.svc.ListClients(key)
return ms.svc.ListClients(key, offset, limit)
}
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
@@ -88,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())
@@ -97,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())
@@ -106,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())
@@ -115,13 +99,13 @@ func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel
return ms.svc.ViewChannel(key, id)
}
func (ms *metricsMiddleware) ListChannels(key string) ([]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())
}(time.Now())
return ms.svc.ListChannels(key)
return ms.svc.ListChannels(key, offset, limit)
}
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
@@ -133,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) {
+31
View File
@@ -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))
}
+6 -6
View File
@@ -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.
@@ -24,8 +24,8 @@ type ChannelRepository interface {
// by the specified user.
One(string, string) (Channel, error)
// All retrieves the channels owned by the specified user.
All(string) []Channel
// All retrieves the subset of channels owned by the specified user.
All(string, int, int) []Channel
// Remove removes the channel having the provided identifier, that is owned
// by the specified user.
+9 -9
View File
@@ -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.
@@ -44,8 +44,8 @@ type ClientRepository interface {
// by the specified user.
One(string, string) (Client, error)
// All retrieves the clients owned by the specified user.
All(string) []Client
// All retrieves the subset of clients owned by the specified user.
All(string, int, int) []Client
// Remove removes the client having the provided identifier, that is owned
// by the specified user.
+3
View File
@@ -0,0 +1,3 @@
// Package clients contains the domain concept definitions needed to support
// Mainflux clients service functionality.
package clients
+8 -7
View File
@@ -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,15 +13,15 @@ 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 {
return &jwtIdentityProvider{}
func New(secret string) clients.IdentityProvider {
return &jwtIdentityProvider{secret}
}
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
@@ -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
}
@@ -2,71 +2,81 @@ package mocks
import (
"fmt"
"strconv"
"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-"
type channelRepositoryMock struct {
mu sync.Mutex
counter int
channels map[string]manager.Channel
channels map[string]clients.Channel
clients clients.ClientRepository
}
// NewChannelRepository creates in-memory channel repository.
func NewChannelRepository() manager.ChannelRepository {
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
return &channelRepositoryMock{
channels: make(map[string]manager.Channel),
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 = strconv.Itoa(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) []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([]clients.Channel, 0)
channels := make([]manager.Channel, 0)
if offset < 0 || limit <= 0 {
return channels
}
// Since IDs starts from 1, shift everything by one.
first := fmt.Sprintf("%s%012d", chanID, offset+1)
last := fmt.Sprintf("%s%012d", chanID, offset+limit+1)
for k, v := range crm.channels {
if strings.HasPrefix(k, prefix) {
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
channels = append(channels, v)
}
}
@@ -79,32 +89,33 @@ 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
}
// Since the current implementation has no way to retrieve a real client
// instance, the implementation will assume client always exist and create
// a dummy one, containing only the provided ID.
channel.Clients = append(channel.Clients, manager.Client{ID: clientId})
client, err := crm.clients.One(owner, clientID)
if err != nil {
return err
}
channel.Clients = append(channel.Clients, client)
return crm.Update(channel)
}
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
channel, err := crm.One(owner, chanId)
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)
}
}
+94
View File
@@ -0,0 +1,94 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/clients"
)
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
const cliID = "123e4567-e89b-12d3-a456-"
type clientRepositoryMock struct {
mu sync.Mutex
counter int
clients map[string]clients.Client
}
// NewClientRepository creates in-memory client repository.
func NewClientRepository() clients.ClientRepository {
return &clientRepositoryMock{
clients: make(map[string]clients.Client),
}
}
func (crm *clientRepositoryMock) ID() string {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter++
return fmt.Sprintf("%s%012d", cliID, crm.counter)
}
func (crm *clientRepositoryMock) Save(client clients.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.clients[key(client.Owner, client.ID)] = client
return nil
}
func (crm *clientRepositoryMock) Update(client clients.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(client.Owner, client.ID)
if _, ok := crm.clients[dbKey]; !ok {
return clients.ErrNotFound
}
crm.clients[dbKey] = client
return nil
}
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
if c, ok := crm.clients[key(owner, id)]; ok {
return c, nil
}
return clients.Client{}, clients.ErrNotFound
}
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []clients.Client {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
clients := make([]clients.Client, 0)
if offset < 0 || limit <= 0 {
return clients
}
// Since IDs start from 1, shift everything by one.
first := fmt.Sprintf("%s%012d", cliID, offset+1)
last := fmt.Sprintf("%s%012d", cliID, offset+limit+1)
for k, v := range crm.clients {
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
clients = append(clients, v)
}
}
return clients
}
func (crm *clientRepositoryMock) Remove(owner, id string) error {
delete(crm.clients, key(owner, id))
return nil
}
@@ -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{}
}
+27
View File
@@ -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
}
+179
View File
@@ -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
}
+215
View File
@@ -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))
}
}
+101
View File
@@ -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
}
+133
View File
@@ -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)
}
}
}
+72
View File
@@ -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
}
+65
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
package manager
package clients
// Hasher specifies an API for generating hashes of an arbitrary textual
// content.
+265
View File
@@ -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
}
+342
View File
@@ -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))
}
}
+17 -120
View File
@@ -1,61 +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.
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 or using an invalid credentials.
500:
$ref: "#/responses/ServiceError"
/clients:
post:
summary: Adds new client
@@ -83,6 +35,8 @@ paths:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
get:
@@ -96,7 +50,7 @@ paths:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Size"
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
responses:
200:
@@ -160,6 +114,8 @@ paths:
description: Missing or invalid access token provided.
404:
description: Client does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
delete:
@@ -206,6 +162,8 @@ paths:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
get:
@@ -219,7 +177,7 @@ paths:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Size"
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
responses:
200:
@@ -283,6 +241,8 @@ paths:
description: Missing or invalid access token provided.
404:
description: Channel does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
delete:
@@ -343,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
@@ -407,12 +324,14 @@ parameters:
type: string
format: uuid
required: true
Size:
name: size
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
@@ -420,6 +339,7 @@ parameters:
in: query
type: integer
default: 0
minimum: 0
required: false
responses:
@@ -523,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
+162
View File
@@ -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
View File
@@ -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))
}
+21 -12
View File
@@ -7,49 +7,57 @@ import (
"os/signal"
"syscall"
"github.com/go-kit/kit/log"
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"
manager "github.com/mainflux/mainflux/manager/client"
log "github.com/mainflux/mainflux/logger"
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),
}
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger := log.New(os.Stdout)
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Log("error", err)
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
pub := nats.NewMessagePublisher(nc)
svc := adapter.New(pub)
@@ -74,8 +82,8 @@ func main() {
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
mc := manager.NewClient(cfg.ManagerURL)
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc))
}()
go func() {
@@ -84,5 +92,6 @@ func main() {
errs <- fmt.Errorf("%s", <-c)
}()
logger.Log("terminated", <-errs)
err = <-errs
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
}
-107
View File
@@ -1,107 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/go-kit/kit/log"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"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.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
if err != nil {
logger.Log("error", 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)
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
logger.Log("terminated", <-errs)
}
+25 -6
View File
@@ -7,10 +7,13 @@ import (
"os/signal"
"syscall"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/normalizer"
nats "github.com/nats-io/go-nats"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
@@ -31,12 +34,11 @@ func main() {
Port: mainflux.Env(envPort, defPort),
}
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
logger := log.New(os.Stdout)
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Log("error", fmt.Sprintf("Failed to connect: %s", err))
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
@@ -45,6 +47,7 @@ func main() {
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
logger.Info(fmt.Sprintf("Normalizer service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
}()
@@ -54,6 +57,22 @@ func main() {
errs <- fmt.Errorf("%s", <-c)
}()
normalizer.Subscribe(nc, logger)
logger.Log("terminated", <-errs)
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "normalizer",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"})
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "normalizer",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"})
normalizer.Subscribe(nc, logger, counter, latency)
err = <-errs
logger.Error(fmt.Sprintf("Normalizer service terminated: %s", err))
}
+143
View File
@@ -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)
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
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"
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
defClientsURL = "localhost:8181"
envPort = "MF_WS_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envClientsURL = "MF_CLIENTS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port string
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.New(os.Stdout)
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
pubsub := nats.New(nc)
svc := adapter.New(pubsub)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "ws_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "ws_adapter",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
}
+19 -12
View File
@@ -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
View File
@@ -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 recieved 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
}
}
+3
View File
@@ -0,0 +1,3 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
+66
View File
@@ -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)
}
+54
View File
@@ -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)
}
+150
View File
@@ -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)
}
}
+225
View File
@@ -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
}
}
}
}
-145
View File
@@ -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
}
}
}
+53
View File
@@ -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
}
-31
View File
@@ -1,31 +0,0 @@
// Package nats contains NATS-specific message repository implementation.
package nats
import (
"encoding/json"
"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 := json.Marshal(msg)
if err != nil {
return err
}
return pub.nc.Publish(topic, data)
}
+26
View File
@@ -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)
}
-30
View File
@@ -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
}
+60
View File
@@ -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": {}
}
}
+13
View File
@@ -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
+58
View File
@@ -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
+201
View File
@@ -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.
+73
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
node_modules
+13
View File
@@ -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
+8
View File
@@ -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;
}
}
+14
View File
@@ -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!');
});
});
+11
View File
@@ -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();
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}
+33
View File
@@ -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
});
};
+10394
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -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"
}
}
+28
View File
@@ -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 } }));
}
};
+9
View File
@@ -0,0 +1,9 @@
{
"/api/*": {
"target": "http://localhost:8180",
"secure":false,
"pathRewrite": {
"^/api": ""
}
}
}
+28
View File
@@ -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 { }
+15
View File
@@ -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>
+6
View File
@@ -0,0 +1,6 @@
.loading {
position: absolute;
width: 100%;
height: 100%;
z-index: 999;
}
+40
View File
@@ -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();
}));
});
+33
View File
@@ -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();
}
}
+90
View File
@@ -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
};
}
}
@@ -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>
@@ -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();
});
});
@@ -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;
}
}

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