Compare commits

..

389 Commits

Author SHA1 Message Date
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
Drasko DRASKOVIC 5cb169f7f8 Add Makefile and ARM support (#178)
* Add Makefile and ARM support

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Add Docker build

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-03-13 09:26:59 +01:00
Dejan Mijić ccd8965d6f Use PostgreSQL as primary persistence solution (#175)
* Use normalizer as stream source

Renamed 'writer' service to 'normalizer' and dropped Cassandra
facilities from it. Extracted the common dependencies to 'mainflux'
package for easier sharing. Fixed the API docs and unified environment
variables.

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

* Use docker build arguments to specify build

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

* Remove cassandra libraries

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

* Update go-kit version to 0.6.0

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

* Fix manager configuration

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

* Refactor docker-compose

Merged individual compose files and dropped external links. Remove CoAP
container since it is not referenced from NginX config at the moment.
Update port mapping in compose and nginx.conf. Dropped bin scripts.
Updated service documentation.

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

* Drop content-type check

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

* Implement users data access layer in PostgreSQL

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

* Bump version to 0.1.0

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

* Use go-kit logger everywhere (except CoAP)

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

* Improve factory methods naming

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

* Implement clients data access layer on PostgreSQL

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

* Make tests stateless

All tests are refactored to use map-based table-driven tests. No
cross-tests dependencies is present anymore.

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

* Remove gitignore

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

* Fix nginx proxying

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

* Mark client-user FK explicit

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

* Update API documentation

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

* Update channel model

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

* Add channel PostgreSQL repository tests

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

* Implement PostgreSQL channels DAO

Replaced update queries with raw SQL. Explicitly defined M2M table due
to difficulties of ensuring the referential integrity through GORM.

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

* Expose connection endpoints

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

* Fix swagger docs and remove DB logging

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

* Fix nested query remarks

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

* Add unique indices

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-03-11 18:06:01 +01:00
Drasko DRASKOVIC b23ecb64e5 Merge pull request #167 from mainflux/coap
Update CoAP README
2018-02-17 16:15:26 +01:00
Drasko DRASKOVIC 03b020ae87 Update CoAP README
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-02-17 16:14:28 +01:00
Drasko DRASKOVIC 255d1c03b1 Merge pull request #162 from chombium/mainflux-161
replaced glide with dep dependency manager
2018-02-14 22:23:58 +01:00
Jovan Kostovski 7fe8100d6b replaced glide with dep dependency manager
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-14 18:28:42 +01:00
Drasko DRASKOVIC 6d9c92596c Merge pull request #158 from bblagojevic94/mainflux-157
Export normalize method from writer service inside cassandra package
2018-02-10 17:50:18 +01:00
Bojan Blagojevic 3ac88b05a8 Export normalize method from writer service inside cassandra package
Signed-off-by: Bojan Blagojevic <datiglavaradi@gmail.com>
2018-02-10 15:45:53 +01:00
Drasko DRASKOVIC f84958834e Merge pull request #148 from chombium/mainflux-147
Various bin script fixes
2018-02-06 10:44:02 +01:00
Jovan Kostovski 7723b92ba7 added the CoAP server
Signed-off-by: Jovan Kostovski <chombium@gmail.com>

Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 02:14:22 +01:00
Jovan Kostovski 3072d054ee added back the coap service
Signed-off-by: Jovan Kostovski <chombium@gmail.com>

Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 02:13:22 +01:00
Jovan Kostovski b4a51ae0a2 reverted the removal of the coap service
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Jovan Kostovski f277074d42 added desc. of the Cassandra create keyspaces cmd
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Jovan Kostovski 2f1b999727 removed the cleaning
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Jovan Kostovski ce86fbbe90 added desc. how to create the Cassandra keyspaces
Signed-off-by: Jovan Kostovski <chombium@gmail.com>

In the README.md it is written that the install script will guide the user how to setup the infrastructure
for running Mainflux, but there was a missing description on which Cassandra keyspacess are needed and how to create them.
2018-02-06 01:48:38 +01:00
Jovan Kostovski 615ba9267d clean up before/after compile and installation
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Jovan Kostovski 10113bd354 fixed the link to the contribution guide
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Jovan Kostovski c691ee06f0 show help and exit if no argument is provided
Signed-off-by: Jovan Kostovski <chombium@gmail.com>

bugfix for the case where the script was started without an argument.
The help was printed, but the _mainflux_docker was called as well, which
was causing the following error:
./mainflux-docker.sh: line 163: $1: unbound variable
2018-02-06 01:48:38 +01:00
Jovan Kostovski 23a3a5f194 added a .gitignore file
Signed-off-by: Jovan Kostovski <chombium@gmail.com>

Added a .gitignore file with the build folder in it, so that
if someone bulds Mainflux localy the build folder does not end
up in the repo.
2018-02-06 01:48:38 +01:00
Jovan Kostovski 6f202a9f83 #105 removed the Coap service from the build
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Jovan Kostovski 74f890128d fixed a typo
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-02-06 01:48:38 +01:00
Nikola Marčetić 6099da59e6 Merge pull request #154 from mainflux/docker-clean
Add `clean` function to docker-compose script
2018-02-05 10:47:31 +01:00
Drasko DRASKOVIC 0255a4cd61 Add clean function to docker-compose script
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-02-05 02:33:51 +01:00
Drasko DRASKOVIC 411cbf575a Merge pull request #146 from mainflux/issue-145
Fix 403 errors handling
2018-01-26 20:56:05 +01:00
Dejan Mijic 5d715aa984 Fix 403 errors handling
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-01-26 20:50:31 +01:00
Drasko DRASKOVIC 30f7dda491 Merge pull request #144 from kelvinji2009/master
fix external_link dependency for http adaptor service
2018-01-18 17:45:27 +01:00
kelvinji ff5d77722c fix external_link dependency for http adaptor service
Signed-off-by: kelvinji <kelvinji2009@gmail.com>
2018-01-14 18:40:20 +08:00
Dejan Mijic b6eac477b4 Fix code linting
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-01-13 18:25:58 +01:00
Dejan Mijić 7cc03be424 Close response body properly (#143) 2018-01-13 18:15:41 +01:00
Drasko DRASKOVIC 4f467dcdde Add HTTP auth check (#128)
* Add HTTP auth check

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Add HTTP auth via Manager Client

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Change the way manager client is injected

Instead of relying on modifying service structure, manager client is
injected through handler factory method, and store as a variable in that
module.

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

* Update dockerignore

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2018-01-07 14:42:38 +01:00
Drasko DRASKOVIC d413508b25 Fix #116: remove base values from C* tables (#140)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2018-01-06 22:36:44 +01:00
Drasko DRASKOVIC 8036c74207 Merge pull request #138 from mainflux/mainflux-137
Remove redundant endpoints from manager
2018-01-05 09:11:10 +01:00
Dejan Mijic 3b57ebf532 Fix manager client implementation
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-29 11:10:31 +01:00
Dejan Mijic 15e9745517 Fix middleware method signature
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-29 10:56:59 +01:00
Dejan Mijic f30c9725aa Move contributing guide to .github folder
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-29 10:49:03 +01:00
Dejan Mijic 4c97094427 Update the API handlers
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-29 10:47:43 +01:00
Dejan Mijic 253a7afcf7 Implement access check based on the API spec
Instead of returning boolean value whether or not the access is granted,
an identifier bound to the provided access key is returned.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-29 10:40:44 +01:00
Dejan Mijic 52ab91c275 Update the API documentation of manager service
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-29 10:22:37 +01:00
oldmikeyang 5899177f57 change the order of message table and remove the time to live (#132) 2017-12-23 22:03:04 +01:00
Drasko DRASKOVIC 3e11147ab5 Merge pull request #131 from bzub/130-docker-script-fix
Search for "mainflux-nginx" instead of "nginx"
2017-12-18 14:49:53 +01:00
bzub c57c9f0a3c Search for "mainflux-nginx" instead of "nginx"
Fixes #130 where the script sees non mainflux-nginx container(s) and
assumes mainflux-nginx is already pulled/created.

Signed-off-by: bzub <bzubrod@gmail.com>
2017-12-16 17:02:10 -06:00
Drasko DRASKOVIC b6b997237e Merge pull request #129 from mainflux/feature-manager-client
Authentication client
2017-12-12 11:57:16 +01:00
Dejan Mijic c11cf5820c Add circuit breaker toward the manager service
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-12 11:20:57 +01:00
Dejan Mijic 28bff54ea6 Add authentication client
Implemented a HTTP client toward the manager service. The client is
intended to be used as a library that will generate requests to the
'/identity' endpoint of a manager service instance selected through
provided URL.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-11 15:22:42 +01:00
Dejan Mijic 00117519c4 Update HTTP adapter's configuration docs
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-12-11 14:18:42 +01:00
Drasko DRASKOVIC 238394e0a7 Merge pull request #125 from manuIO/master
Fix README
2017-11-23 18:03:56 +01:00
Manuel IMPERIALE 7eb12015c0 Fix README
Signed-off-by: Manuel IMPERIALE <manuel.imperiale@gmail.com>
2017-11-22 12:51:32 +01:00
Dejan Mijić 46a7341eba Merge pull request #121 from mainflux/revert-120-mainflux-83
Revert "Consul integration"
2017-10-16 11:28:12 +02:00
Dejan Mijić b651537ba9 Revert "Consul integration" 2017-10-16 11:27:11 +02:00
Drasko DRASKOVIC b9e1c0da81 Merge pull request #120 from mainflux/mainflux-83
Consul integration
2017-10-15 20:05:03 +02:00
Dejan Mijic 3466776152 Register CoAP adapter to Consul
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-15 18:19:23 +02:00
Dejan Mijic 3f9f56d0d4 Register http adapter to consul
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-15 18:08:01 +02:00
Dejan Mijic 2e5316e3fd Register writer to consul
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-15 18:07:52 +02:00
Dejan Mijic 9c429d142d Register manager to Consul
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-15 15:25:55 +02:00
Dejan Mijic 11eed2aa2c Format the startup logs
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-15 14:05:05 +02:00
Dejan Mijić 8019cd731e Merge pull request #101 from mainflux/coap
Add coap support
2017-10-08 10:37:01 +02:00
Drasko DRASKOVIC 923a5aefae Remove println
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-10-08 03:04:49 +02:00
Dejan Mijić 89015b0292 Merge pull request #112 from mainflux/readme
Add features
2017-10-07 22:39:05 +02:00
Drasko DRASKOVIC c7054a07f2 Remove comments, clean code
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-10-07 19:13:20 +02:00
Dejan Mijic c2d6cc511c Ensure codestyle adherence
Simplified code where possible. Fixed golint suggestions regarding the
missing godoc comments and unnecessary initialized variables.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-07 19:13:20 +02:00
Drasko DRASKOVIC 8d4543873f Add features
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-10-07 18:44:12 +02:00
Drasko DRASKOVIC 288f437b4d Merge pull request #114 from mainflux/godoc-fixes
Ensure codestyle adherence
2017-10-06 23:12:42 +02:00
Dejan Mijic 86f563ee10 Ensure codestyle adherence
Simplified code where possible. Fixed golint suggestions regarding the
missing godoc comments and unnecessary initialized variables.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-06 23:03:24 +02:00
Drasko DRASKOVIC dd874008b2 Initialize properly NATS in CoAP adapter
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-10-01 18:33:55 +02:00
Drasko DRASKOVIC 9cec19896e Simplify functions
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-10-01 17:01:50 +02:00
Drasko DRASKOVIC a79a9012e4 Add coap support
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-10-01 16:38:32 +02:00
Dejan Mijic 7c48c1cd78 Remove unused service references
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-01 02:18:03 +02:00
Drasko DRASKOVIC 13e9b440fe Merge pull request #111 from mainflux/mainflux-106
Improves API request validation
2017-10-01 01:48:22 +02:00
Dejan Mijic 481b5b230d Validate incoming requests
All validation has been moved to the API resources layer, i.e. the
domain objects provide validation method, but the service itself assumes
no corrupted values are passed to it.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-01 01:12:06 +02:00
Dejan Mijic da3089cf87 Ensure service interface is properly implemented
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-10-01 01:12:06 +02:00
Drasko DRASKOVIC eb99ef97e3 Merge pull request #110 from mainflux/compose-fix
Fixed compose file, added env var for mqtt-adapter
2017-09-30 22:38:45 +02:00
nmarcetic 1c664a002e Fixed compose file, added env var for mqtt-adapter
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2017-09-30 19:44:11 +02:00
Dejan Mijić 23111608c8 Merge pull request #109 from mainflux/raw
Change writer message format - accept raw messages
2017-09-29 16:35:26 +02:00
Drasko DRASKOVIC 199a44b0a0 Change writer message format - accept raw messages
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-29 16:18:41 +02:00
Drasko DRASKOVIC ea3fb4b781 Merge pull request #107 from mainflux/launch
Fix mainflux-launch.sh script
2017-09-27 20:02:55 +02:00
Drasko DRASKOVIC 778685fca0 Fix mainflux-launch.sh script
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-27 19:03:32 +02:00
Dejan Mijić df61bdccad Merge pull request #99 from mainflux/identity
Add /identity
2017-09-27 15:07:22 +02:00
Drasko DRASKOVIC 82aa89397e Add /identity
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-27 14:31:57 +02:00
Drasko DRASKOVIC 0f2020aeee Merge pull request #105 from mainflux/install
Fix installation and remove coap from Makefile
2017-09-26 18:17:04 +02:00
Drasko DRASKOVIC 499b44fae2 Fix installation and remove coap from Makefile
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-26 16:58:36 +02:00
Dejan Mijić 7721b61fa2 Merge pull request #104 from mainflux/install
Add Makefile and fix installation from sources
2017-09-26 16:51:14 +02:00
Drasko DRASKOVIC f2aa007fab Add Makefile and fix installation from sources
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-26 16:50:14 +02:00
nmarcetic 3ecc79375d Fixed docker-compose.nginx, wrong path maping
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2017-09-26 12:57:58 +02:00
Dejan Mijic f8c41caeb3 Fix documentation links
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-25 13:50:23 +02:00
Nikola Marčetić 0c92be51c8 Merge pull request #100 from mainflux/mainflux-98
Fixes the composition image names
2017-09-25 13:18:40 +02:00
Dejan Mijic 6347d6cbf3 Reorganize utility scripts
Moved docker composition management into 'bin' directory. Removed unused
monitoring composition and fixed versions of NATS, Cassandra and NGINX.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-25 12:29:48 +02:00
Dejan Mijić 0c629cce47 Merge pull request #95 from mainflux/normalizer
Add normalizer
2017-09-24 18:50:02 +02:00
Drasko DRASKOVIC 2554adcc1e Use interface
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-24 17:19:42 +02:00
Drasko DRASKOVIC 3494c7a5e1 Add normalizer
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-24 15:33:58 +02:00
Drasko DRASKOVIC 9afaea24c2 Merge pull request #97 from mainflux/mainflux-96
Fixes inconsisent label cardinality error
2017-09-24 13:41:49 +02:00
Dejan Mijic 7ea0d444bf Fix inconsisent label cardinality error
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-24 07:39:31 +02:00
Dejan Mijic 6dc67f66da Replace travis with semaphore
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 18:20:53 +02:00
Drasko DRASKOVIC c60b124ad2 Merge pull request #94 from mainflux/travis-ci-integration
Travis ci integration
2017-09-23 17:13:53 +02:00
Dejan Mijic 6f97c4588e Add CI script
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 17:10:31 +02:00
Dejan Mijic 75a8cdfe3f Unify service versioning
Services are versioned through top-level package. The package itself
contains an HTTP header that will be bound to the '/version' endpoint of
each of the available services.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 15:52:39 +02:00
Drasko DRASKOVIC bdf5001072 Merge pull request #93 from mainflux/feature-monorepo-migration
Monorepo migration
2017-09-23 14:18:30 +02:00
Dejan Mijic 40d4dd5f2d Fix contributing guide
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 13:44:25 +02:00
Dejan Mijic 4132a5c017 Update readme files for monorepo
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 02:27:49 +02:00
Dejan Mijic 26a2ea1ce1 Integrate http adapter service
Moved main method to top-level 'cmd' directory. Extracted the dockerfile
to the root as well.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 01:57:14 +02:00
Dejan Mijic bcc0bfdc3d Integrate message writer service
Moved main method to top-level cmd directory. Extracted its dockerfile
as well.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 01:22:21 +02:00
Dejan Mijic c966a7802d Integrate manager service
Setup top-level glide dependencies file. Migrated all of the manager
service code into this repository. Fixed docker build procedure.
Extracted executable to the top-level.

Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-23 01:03:27 +02:00
Dejan Mijic 311fe29dc4 Remove lora-related artifacts
Signed-off-by: Dejan Mijic <dejan@mainflux.com>
2017-09-22 23:17:04 +02:00
nmarcetic 623f9b1951 Added db, persistence data from docker shared volume
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2017-09-22 12:19:13 +02:00
Drasko DRASKOVIC e47e5ffc22 Merge pull request #92 from mainflux/docker-cassandra-persistency
Added volume to docker-compose-cassandra in order to support data per…
2017-09-22 12:16:09 +02:00
nmarcetic 3d7c341c5e Added volume to docker-compose-cassandra in order to support data persistency
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2017-09-22 12:11:16 +02:00
Drasko DRASKOVIC 6fb3c3f594 Merge pull request #89 from mainflux/nginx-reverse-proxy
Added Nginx as revrse proxy, Traefik removed. CORS enabled
2017-09-21 23:49:38 +02:00
nmarcetic 79a6fc158b Added Nginx as revrse proxy, Traefik removed. CORS enabled
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2017-09-21 18:50:00 +02:00
Drasko DRASKOVIC cc3283c32b Add NGINX Docker support
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-19 00:28:31 +02:00
Drasko DRASKOVIC fe3ecd5c78 Update README.md 2017-09-19 00:02:46 +02:00
Drasko DRASKOVIC 763f9aa0b7 Update README.md 2017-09-18 21:44:13 +02:00
Drasko DRASKOVIC 0e5f97a8a8 Update README.md 2017-09-18 21:41:57 +02:00
Drasko DRASKOVIC 3eeb2ef10e Add info about mainflux_docker.sh
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-18 21:38:06 +02:00
Drasko DRASKOVIC eff9f62b8b Add monitoring composition
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-18 21:28:21 +02:00
Drasko DRASKOVIC 5cebacdd35 Fix Traefik
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-18 16:33:27 +02:00
Drasko DRASKOVIC 0d3934e9a6 Fix manager bridge network
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-18 15:43:42 +02:00
Drasko DRASKOVIC 01bd08d5b8 Add traefik support
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-18 00:52:16 +02:00
Drasko DRASKOVIC 11f28ef754 Poll on Cassandra ready
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-17 17:23:35 +02:00
Drasko DRASKOVIC 717da672ac Add docker-compose helper script
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-17 03:35:06 +02:00
Drasko DRASKOVIC f21c36fcf6 Update CONTRIBUTING.md 2017-09-11 14:06:01 +02:00
Drasko DRASKOVIC b7863f171c Fix dead link 2017-09-11 14:04:40 +02:00
Drasko DRASKOVIC b7c5f14236 Update ISSUE_TEMPLATE.md 2017-09-11 11:55:32 +02:00
Drasko DRASKOVIC 25a8d94e8a Update ISSUE_TEMPLATE.md 2017-09-11 11:54:44 +02:00
Drasko DRASKOVIC d9ef550697 Update README.md 2017-09-11 11:41:43 +02:00
Drasko DRASKOVIC e967cfe5df Update ISSUE_TEMPLATE.md 2017-09-11 11:02:40 +02:00
Drasko DRASKOVIC 478d22a0d6 Update README.md 2017-09-11 10:37:32 +02:00
Drasko DRASKOVIC d1e450c752 Update README.md 2017-09-11 10:15:22 +02:00
Drasko DRASKOVIC 4a9f1b1414 Update README.md 2017-09-11 09:56:30 +02:00
Drasko DRASKOVIC 656add84f1 Remove depricated auth
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-05 01:40:31 +02:00
Drasko DRASKOVIC 90a141ea41 Fix scripts to use new service names
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-05 01:37:28 +02:00
Drasko DRASKOVIC e297ab4607 Change install script to use new repo names
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-05 01:34:25 +02:00
Drasko DRASKOVIC f8ee020c22 Update README.md 2017-09-04 16:40:14 +02:00
Drasko DRASKOVIC 67844820c3 Update README.md 2017-09-03 01:04:31 +02:00
Drasko DRASKOVIC a473d16cd3 Update README.md 2017-09-03 00:16:10 +02:00
Drasko DRASKOVIC 8c2726200f Update README.md 2017-09-02 23:33:58 +02:00
Drasko DRASKOVIC e93c447d0a Update README.md 2017-09-02 23:32:18 +02:00
Drasko DRASKOVIC 79a6683406 Update README.md 2017-09-02 22:29:46 +02:00
Drasko DRASKOVIC bc55890dc0 Update README.md 2017-09-02 22:28:23 +02:00
Drasko DRASKOVIC e44b035343 Move to new services
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-02 22:22:00 +02:00
Drasko DRASKOVIC 04586424b3 Add composition - towards 1.0.0
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-09-02 02:39:37 +02:00
Drasko DRASKOVIC 1ce3f72d87 Update README.md 2017-08-23 23:35:43 +02:00
Drasko DRASKOVIC 367871d268 Update README.md 2017-08-23 23:34:18 +02:00
Drasko DRASKOVIC b8a67898cc Update README.md 2017-07-08 17:04:35 +02:00
Drasko DRASKOVIC db361038af Update README.md 2017-07-08 17:04:01 +02:00
Drasko DRASKOVIC b71de906b3 Update CONTRIBUTING.md 2017-06-05 21:55:07 +02:00
Drasko DRASKOVIC 43f1f11964 Update README.md 2017-06-03 21:22:17 +02:00
Drasko DRASKOVIC 704b523a22 Update README.md 2017-06-03 21:09:26 +02:00
Drasko DRASKOVIC 890785baa1 Add script that stops system services
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-06-03 01:35:42 +02:00
Drasko DRASKOVIC d9cad2b4b3 Add comments
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-06-03 01:29:56 +02:00
Drasko DRASKOVIC c657eedb6c Add script to launch Go binaries
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-06-03 01:20:10 +02:00
Drasko DRASKOVIC 0a825bcce9 Update README.md 2017-06-02 12:01:18 +02:00
Drasko DRASKOVIC 5ac6ddb3e3 Update README.md 2017-06-02 11:35:41 +02:00
Drasko DRASKOVIC 4fa1f7c6a5 Update README.md 2017-05-24 19:25:14 +02:00
Drasko DRASKOVIC 85093c0b4b Update README.md 2017-05-24 14:56:56 +02:00
Drasko DRASKOVIC 80edbada70 Update README.md 2017-05-24 00:42:55 +02:00
Drasko DRASKOVIC f93c6a4249 Correct auth docker-compose
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-05-24 00:37:21 +02:00
Drasko DRASKOVIC 49249f08e2 Update README.md 2017-05-24 00:13:35 +02:00
Drasko DRASKOVIC 33ff1e7529 Update README.md 2017-05-24 00:12:12 +02:00
Drasko DRASKOVIC d9df54046c Update README.md 2017-05-24 00:10:56 +02:00
Drasko DRASKOVIC e73132e748 Switch to new architecture
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-05-24 00:02:53 +02:00
Darko Draskovic 23c8f1f53f Add mainflux-manager docker container
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2017-05-23 20:01:10 +02:00
Drasko DRASKOVIC 811b5b787b Add coap server
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-05-01 02:36:25 +02:00
Drasko DRASKOVIC 4332a5ea4b Fix docker-compose
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-04-24 00:46:36 +02:00
Drasko DRASKOVIC 6e3b8407d9 Add support for new architecture
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-04-23 19:57:59 +02:00
Drasko DRASKOVIC 2f96df1c35 Use dockers from loraserver-docker
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-03-28 01:16:33 +02:00
Drasko DRASKOVIC 90cdfb300e Add LoRaWAN Server written in Erlang
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-03-20 01:21:40 +01:00
Drasko DRASKOVIC 318292703a Add port mapping
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-03-19 03:13:48 +01:00
Drasko DRASKOVIC ce40a0a7a5 Update README.md 2017-03-11 01:39:05 +01:00
Drasko DRASKOVIC 9f4acaf7a8 Update README.md 2017-03-01 23:58:00 +01:00
Drasko DRASKOVIC 068d3b6602 Add lora http certs, fix docekr-compose
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-02-11 18:53:30 +01:00
Drasko DRASKOVIC 09c063496b Add dockerize
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-02-10 01:42:43 +01:00
Drasko DRASKOVIC 56286610d9 Switch to version 2 to avoid circular dependencies
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-02-10 00:28:20 +01:00
Drasko DRASKOVIC 669a3b67e5 Add preliminary LoRa Server support
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-02-09 23:26:09 +01:00
Drasko DRASKOVIC 3d4d5291c3 Change links
Core is no longer connected to MQTT server - they
all communicate via NATS.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-02-03 19:30:44 +01:00
Drasko DRASKOVIC b84f268e14 Add GitHub admin files
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2017-02-01 22:56:06 +01:00
Drasko DRASKOVIC 637eb34178 Merge pull request #76 from darkodraskovic/master
Fix typo
2017-01-18 00:14:42 +01:00
Darko Draskovic e0fbc05c4d Fix typo
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2017-01-18 00:01:23 +01:00
Drasko DRASKOVIC c64337bf03 Update README.md 2017-01-15 00:57:59 +01:00
Drasko DRASKOVIC ad91c25082 Update README.md 2016-12-30 20:41:40 +01:00
Drasko DRASKOVIC e775c2a94b Kill whitespaces
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-30 18:18:09 +01:00
Drasko DRASKOVIC cfa40c70fb Fix comments
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-30 18:15:45 +01:00
Drasko DRASKOVIC 691dcbeb80 Add script to install from sources
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-30 18:09:58 +01:00
Drasko DRASKOVIC 2a07613c75 Add Redis, correct Nginx
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-28 22:54:22 +01:00
Drasko DRASKOVIC abd8c9b6c9 Add docker-compose with Auth and NGINX
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-28 18:17:40 +01:00
Drasko DRASKOVIC b60341db1d Add NATS info 2016-12-04 20:32:20 +01:00
Drasko DRASKOVIC b2417b9bd6 Use correct NATS Docker image
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-04 20:06:06 +01:00
Drasko DRASKOVIC ad0278005c Lock NATS version to latest stable
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-12-04 19:36:28 +01:00
Drasko DRASKOVIC ff238a3491 Update README.md 2016-12-03 18:49:41 +01:00
Nikola 15240dc391 Added mainflux-mqtt links in compose file.
Signed-off-by: Nikola <nikola.marcetic@nsystems.rs>
2016-11-27 19:49:02 +01:00
Drasko DRASKOVIC 4e8e7dbf4e Change NATS monitoring port
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-27 16:11:42 +01:00
Drasko DRASKOVIC e0e17f0e18 Go NATS
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-27 12:56:52 +01:00
Drasko DRASKOVIC e389395d4e Add Redis
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-26 01:44:35 +01:00
Drasko DRASKOVIC 38ad5172e1 Update README.md 2016-11-24 02:23:24 +01:00
Drasko DRASKOVIC 8e98cb5e0c Update README.md 2016-11-24 02:22:40 +01:00
Drasko DRASKOVIC 939f2612dd Update README.md 2016-11-24 02:19:06 +01:00
Drasko DRASKOVIC 133ca7116a Update docker-compose.yml 2016-11-21 22:53:26 +01:00
Drasko DRASKOVIC a005fd8398 Use mainflux-mqtt
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-21 22:52:14 +01:00
Drasko DRASKOVIC 13417691ad Update 2016-11-12 19:49:58 +01:00
Drasko DRASKOVIC d71eb8b20f Update README.md 2016-11-12 19:49:23 +01:00
Drasko DRASKOVIC f90d043b29 Update README.md 2016-11-12 19:44:51 +01:00
Drasko DRASKOVIC d647f62a27 Update README.md 2016-11-12 19:39:53 +01:00
Drasko DRASKOVIC 1b938002b5 Update README.md 2016-11-12 19:39:18 +01:00
Drasko DRASKOVIC 0e34885c73 Update README.md 2016-11-12 19:29:14 +01:00
Drasko DRASKOVIC f58915dd79 Remove not-needed badges 2016-11-12 19:16:27 +01:00
Drasko DRASKOVIC ed7b640700 Not needed any more 2016-11-12 19:15:55 +01:00
Drasko DRASKOVIC 9363cb6804 Correct docker-compose for core microservice
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-12 19:13:10 +01:00
Drasko DRASKOVIC 48779e856f Remove all from main repo
We will now use dedicated repos for microservices.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-12 19:11:40 +01:00
Drasko DRASKOVIC 20ce5268d4 Update README.md 2016-11-06 16:44:59 +01:00
Drasko DRASKOVIC a6aca34769 Update README.md 2016-11-05 23:33:21 +01:00
Drasko DRASKOVIC 722381a637 Update README.md 2016-11-05 23:25:48 +01:00
Drasko DRASKOVIC a93de22af7 Update README.md 2016-11-05 02:51:13 +01:00
Drasko DRASKOVIC 9561949e60 Fix Go report
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-05 01:36:12 +01:00
Drasko DRASKOVIC a3886c64e9 Improve Go report
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-05 00:51:26 +01:00
Drasko DRASKOVIC d38ea79dde Gofmt
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-04 23:10:13 +01:00
Drasko DRASKOVIC c12a88d12f Fix typo
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-04 22:30:19 +01:00
Drasko DRASKOVIC 6b54d38d53 Add dockerize to docker-commpose.yml
As described in issue #67, the order of services is important,
and mainflux-core must wait for emqtt to be available on port 1883.

docker-compose do not guarantees the order of starting services,
but we must use dockerize to check if one service is avilable before
we can start other service that depends on it.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-11-02 22:55:04 +01:00
Drasko DRASKOVIC ad6f9ee721 Update README.md 2016-11-02 01:31:07 +01:00
Drasko DRASKOVIC d0ff035688 Update README.md 2016-11-02 01:30:46 +01:00
Drasko DRASKOVIC fa9bc47482 Update README.md 2016-11-02 01:29:33 +01:00
Drasko DRASKOVIC b605341f73 Update MAINTAINERS 2016-10-29 20:15:38 +02:00
Drasko DRASKOVIC c9e7fb112a Update README.md 2016-10-29 20:07:58 +02:00
Drasko DRASKOVIC 1b2b58f6b0 Update README.md 2016-10-25 01:58:34 +02:00
Drasko DRASKOVIC 73f7af3eeb Expose HTTP ports
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-17 22:06:04 +02:00
Drasko DRASKOVIC cc50880217 Fix MQTT port
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-17 20:18:46 +02:00
Drasko DRASKOVIC bbc66553b2 Fix Glide installation
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-13 22:55:20 +02:00
Drasko DRASKOVIC 3f0eaaec31 Use Glide's novendor
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-13 22:47:22 +02:00
Drasko DRASKOVIC 1a6102c5fe Update README.md 2016-10-13 00:25:31 +02:00
Drasko DRASKOVIC d2b03d09ae Update .travis.yml 2016-10-13 00:08:49 +02:00
Drasko DRASKOVIC 2cd0e70d80 Update .travis.yml 2016-10-13 00:05:05 +02:00
Drasko DRASKOVIC 05f0fc776d Update .travis.yml 2016-10-12 23:59:13 +02:00
Drasko DRASKOVIC f7a21d43da Fix typo 2016-10-12 23:57:03 +02:00
Drasko DRASKOVIC 520006f06d Add comment for .travis.yml
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-12 23:54:39 +02:00
Drasko DRASKOVIC 180aaeae6f Remove vendor before build
Because of many problems with vedored package testing (missing
DB connections, etc), remove vendoring for Travis build

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-12 23:45:26 +02:00
Drasko DRASKOVIC e847ac251a Bump Go version in Travis to 1.7
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-12 23:31:25 +02:00
Drasko DRASKOVIC 808dbb82df Updated vendor packages
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-12 21:55:14 +02:00
Drasko DRASKOVIC 1cf4558d11 Merge pull request #65 from nmarcetic/master
glide.yaml pull latest version of paho.mqtt from master branch
2016-10-12 19:41:47 +02:00
Nikola Marčetić b1fab11c32 Pulled latest master branch version of paho mqtt with glide
Signed-off-by: Nikola Marčetić <nikola.marcetic@nsystems.rs>
2016-10-12 17:45:05 +02:00
Nikola Marčetić 63a97748b6 Changed glide.yml, set master branch for version
Signed-off-by: Nikola Marčetić <nikola.marcetic@nsystems.rs>
2016-10-12 17:43:37 +02:00
Drasko DRASKOVIC 122179b8c5 Merge pull request #63 from nmarcetic/master
Fixed docker-compose and updated paho package, to resolve travis issue https://github.com/eclipse/paho.mqtt.golang/issues/62
2016-10-12 16:56:28 +02:00
Nikola Marčetić bb5f4c1196 Updated paho package, to resolve travis issue
Signed-off-by: Nikola Marčetić <nikola.marcetic@ntsystems.rs>
2016-10-12 15:26:27 +02:00
Nikola Marčetić 4f5ef30b30 Merge branch 'master' of https://github.com/mainflux/mainflux 2016-10-12 15:22:04 +02:00
Drasko DRASKOVIC d1c44a5413 Removed Sevice field - we do not need it for now
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-10 20:28:51 +02:00
Drasko DRASKOVIC 1d74046154 Add Glide and vendoring
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-09 20:48:34 +02:00
Nikola Marcetic de236fcc90 Fixed docker-compose 2016-10-08 16:48:27 +02:00
Drasko DRASKOVIC 9de6bc1448 Prefix DB entries with channel name and unit
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-08 02:42:07 +02:00
Drasko DRASKOVIC ce928986ba Initialize service and online params
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-08 02:39:30 +02:00
Drasko DRASKOVIC 37399c8944 Limit the GET counts. Use service.
Now GET results for nb of returned channels and
nb of their values can be limited via HTTP req param.

Also, `service` is used to construct MQTT topic.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-08 02:37:09 +02:00
Drasko DRASKOVIC 849ce6e1ad Add channel name and unit (used as bn and bu)
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-08 02:35:30 +02:00
Drasko DRASKOVIC 96fd2bf2b5 Added service field. Simplified device model.
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-08 02:34:19 +02:00
Drasko DRASKOVIC 62c43f1ba4 Move "device" param to payload 2016-10-05 20:16:23 +02:00
Drasko DRASKOVIC 3db4ab8440 Move Dockerfile to root folder
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-04 23:51:44 +02:00
Drasko DRASKOVIC 5d7d5d43ff Update Dockerfile 2016-10-04 23:44:40 +02:00
Drasko DRASKOVIC 2d8ddd7b0c Organize Docker stuff
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-04 23:28:08 +02:00
Drasko DRASKOVIC 779b254c04 Correct test to use stdlib approach
Corrected server test to use std lib strategy

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-04 23:06:55 +02:00
Drasko DRASKOVIC f3f2586f03 Remove Iris, part 2
Removed Iris from /channels, switched to stdlib

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-04 22:47:47 +02:00
Drasko DRASKOVIC d02009744e Remove Iris, part 1
Remove iris and switch to more Go idiomatic API of std lib.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-04 21:55:07 +02:00
Drasko DRASKOVIC d38b125fb7 Remove print of config
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-02 22:57:06 +02:00
Drasko DRASKOVIC 551172ed0a Abandon YAML and switch to TOML
Mainflux has very simple configuration. No need for all the YAML
structures. YAML is complex and overkill. Keep it lean.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-10-02 21:20:56 +02:00
Drasko DRASKOVIC 04710ad019 Remove lite, correct /channels
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 23:48:01 +02:00
Drasko DRASKOVIC 57a4535bc4 Change /channels API
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 23:03:54 +02:00
Drasko DRASKOVIC 52f0017be5 Correct banner
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 22:47:55 +02:00
Drasko DRASKOVIC 2074bac535 Correct banner
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 21:56:01 +02:00
Drasko DRASKOVIC 106c8996d4 Add schema
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 21:55:48 +02:00
Drasko DRASKOVIC fd0ca0e6d6 Add Travis
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 21:28:42 +02:00
Drasko DRASKOVIC bd4a97fdbe Add .dockerignore
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 21:28:42 +02:00
Drasko DRASKOVIC 120acd8564 Update README.md 2016-09-20 21:25:46 +02:00
Drasko DRASKOVIC 566d8ea254 Update README.md 2016-09-20 21:24:27 +02:00
Drasko DRASKOVIC aa3e23906f Correct MAINTAINERS
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 21:21:25 +02:00
Drasko DRASKOVIC be956accd6 Update README.md 2016-09-20 21:01:33 +02:00
Drasko DRASKOVIC c06d62a8a7 Update README.md 2016-09-20 20:07:54 +02:00
Drasko DRASKOVIC 20b0207a3c Update README.md 2016-09-20 20:03:31 +02:00
Drasko DRASKOVIC 469b8f0c79 🐭 GO GO GO!
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-09-20 19:24:54 +02:00
Drasko DRASKOVIC f3dfcd7e7e Update README.md 2016-08-23 10:50:31 +02:00
Drasko DRASKOVIC b8f938e180 Update README.md 2016-08-23 00:35:56 +02:00
Drasko DRASKOVIC bbc45150cc Update README.md 2016-08-23 00:24:31 +02:00
Drasko DRASKOVIC d429dec1d8 Update README.md 2016-08-23 00:21:32 +02:00
Drasko DRASKOVIC eaf6863d1c Update README.md 2016-08-22 23:54:41 +02:00
Drasko DRASKOVIC 0bc2fd8ff6 Update configs to use docker-compose hostnames
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-08-14 20:07:20 +02:00
Drasko DRASKOVIC 2afd866ec3 Add InfluxDB
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-08-14 20:06:59 +02:00
Drasko DRASKOVIC 9e64f3a7d3 Migrate to Go
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-08-09 21:28:44 +02:00
Drasko DRASKOVIC 3ef7035689 Update README.md 2016-08-08 23:38:08 +02:00
Drasko DRASKOVIC b7be831b8b Update README.md 2016-08-08 23:37:24 +02:00
Drasko DRASKOVIC 6a24aa91e6 Update README.md 2016-05-18 23:20:39 +02:00
Drasko DRASKOVIC b88fc346d0 Update README.md 2016-05-18 23:18:40 +02:00
Drasko DRASKOVIC fb4140d69c Add Locust test template
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-05-18 20:41:21 +02:00
Drasko DRASKOVIC f92db09290 Add Postman HTTP RESTful API testing collections
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-05-18 20:38:36 +02:00
Drasko DRASKOVIC b76461e773 Add Vim ignore
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-05-18 20:37:46 +02:00
Drasko DRASKOVIC bc54a9ad6a Fix Docker host names
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-05-18 20:35:58 +02:00
Drasko DRASKOVIC 3bb18620b2 Add NATS configs
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-05-17 23:24:19 +02:00
Drasko DRASKOVIC 2f1e5163b5 Update README.md 2016-05-10 22:32:09 +02:00
Drasko DRASKOVIC f06687e2f4 Update README.md 2016-05-10 22:30:43 +02:00
Drasko DRASKOVIC 460874b9ab Add docker-compose with local config
This docker-commpose takes local `config` dir and
mounts it into containers, so that microservices can take
locally customized configuration upon deployment.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 23:58:57 +02:00
Drasko DRASKOVIC fc4c658ff2 Add docker-compose with local config
This docker-commpose takes local `config` dir and
mounts it into containers, so that microservices can take
locally customized configuration upon deployment.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 23:53:26 +02:00
Drasko DRASKOVIC 8f4ef8464e Correct docker-compose configs
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 23:50:18 +02:00
Drasko DRASKOVIC 454760e62e Add microservices local configurations
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 23:46:29 +02:00
Drasko DRASKOVIC 797a804cda Use microservices
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 23:36:20 +02:00
Drasko DRASKOVIC 8aa4bbc053 Update README.md 2016-04-28 22:57:41 +02:00
Drasko DRASKOVIC 0934fbe834 Update README.md 2016-04-28 22:56:58 +02:00
Drasko DRASKOVIC 09907fd78f Update README.md 2016-04-28 22:25:08 +02:00
Drasko DRASKOVIC dcd45d8bda Remove .dockerignore and .travis.yml
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 22:23:05 +02:00
Drasko DRASKOVIC daf9f39a11 Update README.md 2016-04-28 22:19:08 +02:00
Drasko DRASKOVIC 1750395240 Update README.md 2016-04-28 22:16:01 +02:00
Drasko DRASKOVIC 4c013fac6f Update README.md 2016-04-28 22:07:16 +02:00
Drasko DRASKOVIC d950002593 Refractored in Microservices
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-28 21:49:54 +02:00
Drasko DRASKOVIC 0dca37b656 Correct Gulpfile
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-27 23:12:31 +02:00
Drasko DRASKOVIC cf17b53bb6 Add MQTT, HTTP and WS API servers
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-27 23:05:42 +02:00
Drasko DRASKOVIC 8fee81d8bc Move config.js in root dir, add MQTT and HTTP conf
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-27 23:05:42 +02:00
Drasko DRASKOVIC eff9fd2411 Add NATS broker client
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-27 23:05:42 +02:00
Drasko DRASKOVIC 396727b4d5 Add MQTT and WS server
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-27 23:05:42 +02:00
Nikola Marcetic 2fb79f1a19 --Readme changed 2016-04-22 10:59:42 +02:00
Drasko DRASKOVIC 7d332297fc Change compostion ports
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-20 21:00:38 +02:00
Drasko DRASKOVIC bb0ead885f Make app port more configurable
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-20 20:58:41 +02:00
Drasko DRASKOVIC 00c6cb77c6 Revert "Move back to mongoose. Remove JWT."
This reverts commit 129e911652.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-20 20:28:39 +02:00
Drasko DRASKOVIC 021427431a Correct Docker files
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-20 00:20:57 +02:00
Drasko DRASKOVIC 129e911652 Move back to mongoose. Remove JWT.
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2016-04-19 23:14:28 +02:00
Drasko DRASKOVIC 41c1bc03b4 Update README.md 2016-02-15 20:06:54 +01:00
Drasko DRASKOVIC 88405089f6 Update README.md 2016-02-15 01:12:00 +01:00
Drasko DRASKOVIC 9e8888fdff Merge pull request #41 from drasko/master
Correct GET on single device
2015-12-08 02:44:50 +01:00
Drasko DRASKOVIC bd16fa2616 Correct GET on single device
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-08 02:39:16 +01:00
Drasko DRASKOVIC 8eee6f1bcf Merge pull request #40 from drasko/master
Update README.md
2015-12-07 01:33:12 +01:00
Drasko DRASKOVIC fbb527b435 Update README.md 2015-12-07 01:29:56 +01:00
Drasko DRASKOVIC b046ce5dd7 Merge pull request #39 from drasko/master
Update README.md
2015-12-07 01:19:44 +01:00
Drasko DRASKOVIC 08c3bac64e Update README.md 2015-12-07 01:03:28 +01:00
Drasko DRASKOVIC f88a913333 Merge pull request #38 from drasko/master
Correct Docker stuff
2015-12-07 00:34:35 +01:00
Drasko DRASKOVIC ef00dac107 Correct Docker stuff
This commit corrects docker-compose.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-07 00:27:52 +01:00
Drasko DRASKOVIC 87fa44f9c9 Merge pull request #37 from drasko/master
Cosmetics
2015-12-06 19:48:04 +01:00
Drasko DRASKOVIC 7eb4c34ab1 Cosmetics
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-06 19:42:27 +01:00
Drasko DRASKOVIC f3fd689305 Merge pull request #36 from drasko/master
JWT corrections
2015-12-06 19:42:13 +01:00
Drasko DRASKOVIC 55b28f90f0 Add JWT data in the signature
This commit adds Mainflux metadata embedded in JWT.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-06 14:57:40 +01:00
Drasko DRASKOVIC 40194aeb9b Create logger as a separate file
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-06 14:57:40 +01:00
Drasko DRASKOVIC b930a8f75f Add utils dir and generateToken.js utility
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-06 14:57:40 +01:00
Drasko DRASKOVIC 469880a3fa Merge pull request #35 from drasko/master
Add `bunyan` logger
2015-12-06 14:54:26 +01:00
Drasko DRASKOVIC 5efcacc731 Add bunyan logger
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-05 19:02:49 +01:00
Drasko DRASKOVIC 7382ab9073 Merge pull request #34 from drasko/master
Swagger file updates
2015-12-05 19:02:41 +01:00
Drasko DRASKOVIC 0faf6d85c0 Update swagger.yaml
Add tags
2015-12-05 01:23:59 +01:00
Drasko DRASKOVIC e2ab3e19e6 Correct Swagger
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-05 01:15:29 +01:00
Drasko DRASKOVIC 980f6ac525 Merge pull request #33 from drasko/master
Correct Swagger file
2015-12-04 01:16:44 +01:00
Drasko DRASKOVIC e7e4da29bb Add jwt
Devices routes are now JWT protected. We are using `restify-jwt`
which is using `jsonwebtoken` behind.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-04 00:54:39 +01:00
Drasko DRASKOVIC 54913c7d23 Correct Swagger file
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-04 00:54:39 +01:00
Drasko DRASKOVIC be79cae443 Merge pull request #32 from drasko/master
Use restify and mongojs. Change structure.
2015-12-02 22:21:10 +01:00
Drasko DRASKOVIC 59da29ff2b Use restify and mongojs. Change structure.
This commit introducess massive change:
- Replace Express by Restify for simplicity and lean approach
- Replace Mongoose by MongoJS for DB schema-less paradigm
- Introduce `controllers` dir, and isolate all routes in `routes.js`

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-02 00:17:10 +01:00
Drasko DRASKOVIC 744068bda8 Correct README
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-12-02 00:17:10 +01:00
Nikola 122c02769d Merge pull request #31 from drasko/master
Launch test with `npm test`
2015-11-02 10:55:44 +01:00
Drasko DRASKOVIC 276a505eeb Add gulp-exit - gulp exits after finished tests
Some plugins, like gulp-mocha, have problems with a proper termination
after finishing the task. This plugin guarantees that the task will exit
successfully.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-02 01:49:39 +01:00
Drasko DRASKOVIC 2e27375adb Add server.close() in apiTest.js
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-02 01:49:39 +01:00
Drasko DRASKOVIC 38311af348 Add mongodb in Travis build
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-02 01:49:39 +01:00
Drasko DRASKOVIC 455b469bc2 Add gulp locally
Gulp has to be added both globally and locally for
Travis to work.
Aome explanations:
http://blog.dwaynecrooks.com/post/110903139442/why-do-we-need-to-install-gulp-globally-and

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-02 01:49:39 +01:00
Drasko DRASKOVIC 63281acac5 Add local mocha and fix .travis.yml
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-02 01:49:39 +01:00
Drasko DRASKOVIC 253495d5bb Launch test with npm test
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-02 01:49:39 +01:00
Drasko DRASKOVIC 66ef92dbf7 Merge pull request #30 from drasko/master
Add Travis YAML
2015-11-01 15:54:21 +01:00
Drasko DRASKOVIC f2fd735dbd Add Travis YAML
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-01 15:51:39 +01:00
Drasko DRASKOVIC 4e5edad061 Merge pull request #29 from drasko/master
Add Mocha, Chai and Supertest combo for testing
2015-11-01 15:33:52 +01:00
Drasko DRASKOVIC cb6471790b Add test task in gulp
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-01 15:27:49 +01:00
Drasko DRASKOVIC 5e31f45b27 Add Mocha, Chai and Supertest combo for testing
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-11-01 03:43:23 +01:00
Drasko DRASKOVIC 51898276f3 Add Community links 2015-10-17 00:03:21 +02:00
Drasko DRASKOVIC 9f3a728ccb Merge pull request #25 from nmarcetic/master
-- Hello JWT
2015-10-13 16:47:58 +02:00
nmarcetic f36013016e -- Hello JWT
Signed-off-by: nmarcetic <nikola.marcetic@ntsystems.rs>
2015-10-13 16:35:32 +02:00
Nikola b520216351 Merge pull request #21 from drasko/master
Add license to package.json. Correct HTTP headers.
2015-10-12 20:48:08 +02:00
Drasko DRASKOVIC 92b4a70528 Correct headers for token. Send full device JSON on creation, update and delete
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-10-11 22:35:05 +02:00
Drasko DRASKOVIC fafa623680 Add license to package.json
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-10-11 22:35:05 +02:00
Drasko DRASKOVIC e6203967bb Merge pull request #17 from drasko/master
Add comments for device model, fix typos
2015-10-10 01:40:52 +02:00
Drasko DRASKOVIC 3b86bf249c Add comments for device model, fix typos
Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
2015-10-10 01:37:50 +02:00
Drasko DRASKOVIC 700a34b589 Merge pull request #16 from drasko/master
Update README.md
2015-10-10 01:11:01 +02:00
Drasko DRASKOVIC a3a2c1f449 Update README.md 2015-10-10 01:06:40 +02:00
Drasko DRASKOVIC 910791c8fa Merge pull request #15 from drasko/master
Update README.md
2015-10-10 00:56:17 +02:00
Drasko DRASKOVIC dd806d2a2e Update README.md 2015-10-10 00:55:04 +02:00
Drasko DRASKOVIC 83e439bb5a Merge pull request #14 from drasko/master
Switch LICENSE to Apache v2.0
2015-10-09 23:40:59 +02:00
Drasko DRASKOVIC 45451c8266 Switch LICENSE to Apache v2.0 2015-10-09 23:35:23 +02:00
Drasko DRASKOVIC 47597331f1 Add NodeJS styleguide 2015-10-05 20:07:01 +02:00
Drasko DRASKOVIC 8d914647c8 Corrected typo 2015-10-05 20:04:33 +02:00
Drasko DRASKOVIC 4c7b046824 Merge pull request #13 from drasko/master
LICENSE, CONTRIBUTING and MAINTAINERS files
2015-10-04 05:14:16 +02:00
Drasko DRASKOVIC 46498c152f LICENSE, CONTRIBUTING and MAINTAINERS files
Update MAINTAINERS

Update CONTRIBUTING.md

Update CONTRIBUTING.md

Update CONTRIBUTING.md

Update CONTRIBUTING.md
2015-10-04 05:10:45 +02:00
Drasko DRASKOVIC 427d03fa0a Update README.md
Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md
2015-10-03 01:56:02 +02:00
The Gitter Badger 1c9c5e6605 Add Gitter badge 2015-10-03 01:56:02 +02:00
Drasko DRASKOVIC 014d13298a Correct comments 2015-10-03 01:56:02 +02:00
Drasko DRASKOVIC 9d8004ee4c Add device schema mock-up 2015-10-03 01:56:02 +02:00
janko-isidorovic a2d57cae38 Added Visual Studio files to .gitignore 2015-10-03 01:56:02 +02:00
nmarcetic 3e5a93efbf Docker file fix 2015-07-28 01:10:32 +02:00
nmarcetic c137bda7c4 -- Readme update 2015-07-28 01:01:00 +02:00
nmarcetic 70805a0b2a -- Hello docker-compose 2015-07-28 00:50:20 +02:00
Nikola 8979bb96db Merge pull request #2 from drasko/master
Config and routes directory structure + Docker file
2015-07-27 12:50:45 +02:00
Drasko DRASKOVIC fc410bd89f Added Docker file 2015-07-26 23:39:09 +02:00
Drasko DRASKOVIC c8c61bfe4a Added config and routes dirs 2015-07-26 23:37:22 +02:00
Drasko DRASKOVIC ac018a665c Merge pull request #1 from drasko/master
Several commits
2015-07-07 11:50:51 +02:00
Drasko DRASKOVIC 17adff355c Added gulpfile 2015-07-07 00:31:31 +02:00
Drasko DRASKOVIC 38c08359a9 Added MIT LICENSE.md file 2015-07-06 23:51:10 +02:00
Drasko DRASKOVIC 12fc1e4bcc Added README.md file 2015-07-06 23:43:19 +02:00
Drasko DRASKOVIC 5711803c8d Added more stuff to .gitignore 2015-07-06 23:33:01 +02:00
Drasko DRASKOVIC 7d5b901dc9 Initial commit 2015-07-06 22:32:02 +02:00
1246 changed files with 336484 additions and 13462 deletions
+1
View File
@@ -0,0 +1 @@
* @mainflux/maintainers
+98
View File
@@ -0,0 +1,98 @@
# 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.
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
[abuse@mainflux.com](mailto:abuse@mainflux.com).
## Reporting issues
Reporting issues is a great way to contribute to the project. We always appreciate a well-written,
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
problem you are facing.
A good bug report shouldn't leave others needing to chase you up for more information. Please try to
be as detailed as possible. The following questions might serve as a template for writing a detailed
reports:
- What were you trying to achieve?
- What are the expected results?
- What are the received results?
- What are the steps to reproduce the issue?
- In what environment did you encounter the issue?
## Pull requests
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should
remain focused in scope and avoid containing unrelated commits.
**Please ask first** before embarking any significant pull request (e.g. implementing new features,
refactoring code etc.), otherwise you risk spending a lot of time working on something that the
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)
- [Google's JavaScript styleguide](https://google.github.io/styleguide/jsguide.html)
Adhering to the following process is the best way to get your work included in the project:
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure
the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/mainflux.git
# Navigate to the newly cloned directory
cd mainflux
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/mainflux/mainflux.git
```
2. If you cloned a while ago, get the latest changes from upstream:
```bash
git checkout master
git pull upstream master
```
3. Create a new topic branch from `master` using the naming convention `mainflux-[issue-num]` to
help us keep track of your contribution scope:
```bash
git checkout -b mainflux-[issue-num]
```
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.
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/).
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`.
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.
+30
View File
@@ -0,0 +1,30 @@
<!--
The GitHub issue tracker is for bug reports and feature requests. General support can be found at
the following locations:
- Google group - https://groups.google.com/forum/#!forum/mainflux
- Gitter - https://gitter.im/mainflux/mainflux
-->
**FEATURE REQUEST**
1. Is there an open issue addressing this request? If it does, please add a "+1" reaction to the
existing issue, otherwise proceed to step 2.
2. Describe the feature you are requesting, as well as the possible use case(s) for it.
3. Indicate the importance of this feature to you (must-have, should-have, nice-to-have).
**BUG REPORT**
1. What were you trying to achieve?
2. What are the expected results?
3. What are the received results?
4. What are the steps to reproduce the issue?
5. In what environment did you encounter the issue?
6. Additional information you deem important:
+1
View File
@@ -0,0 +1 @@
build
View File
-1
View File
@@ -1 +0,0 @@
docs.api.magistrala.absmach.eu
Generated
+391
View File
@@ -0,0 +1,391 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/Azure/go-ansiterm"
packages = [
".",
"winterm"
]
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
[[projects]]
name = "github.com/Microsoft/go-winio"
packages = ["."]
revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f"
version = "v0.4.7"
[[projects]]
branch = "master"
name = "github.com/Nvveen/Gotty"
packages = ["."]
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
[[projects]]
name = "github.com/asaskevich/govalidator"
packages = ["."]
revision = "73945b6115bfbbcc57d89b7316e28109364124e1"
version = "v7"
[[projects]]
branch = "master"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "61153c768f31ee5f130071d08fc82b85208528de"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/cisco/senml"
packages = ["."]
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
[[projects]]
branch = "master"
name = "github.com/containerd/continuity"
packages = ["pathdriver"]
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
packages = ["."]
revision = "e137e310a99955be01d60b2237da89153ea40987"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
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"
[[projects]]
name = "github.com/docker/go-connections"
packages = ["nat"]
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0"
[[projects]]
name = "github.com/docker/go-units"
packages = ["."]
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
version = "v0.3.2"
[[projects]]
branch = "master"
name = "github.com/dustin/go-coap"
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 = [
"endpoint",
"log",
"metrics",
"metrics/internal/lv",
"metrics/prometheus",
"transport/http"
]
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
version = "v0.6.0"
[[projects]]
name = "github.com/go-logfmt/logfmt"
packages = ["."]
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
version = "v0.3.0"
[[projects]]
name = "github.com/go-stack/stack"
packages = ["."]
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
version = "v1.7.0"
[[projects]]
name = "github.com/go-zoo/bone"
packages = ["."]
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
version = "1.2"
[[projects]]
name = "github.com/gogo/protobuf"
packages = ["proto"]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
[[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"
]
revision = "58e34726dfc069b558038efbaa25555f182d1f7a"
[[projects]]
branch = "master"
name = "github.com/jinzhu/inflection"
packages = ["."]
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
[[projects]]
branch = "master"
name = "github.com/kr/logfmt"
packages = ["."]
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid"
]
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/nats-io/go-nats"
packages = [
".",
"encoders/builtin",
"util"
]
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
version = "v1.4.0"
[[projects]]
name = "github.com/nats-io/nuid"
packages = ["."]
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
version = "v1.0.0"
[[projects]]
name = "github.com/opencontainers/go-digest"
packages = ["."]
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
version = "v1.0.0-rc1"
[[projects]]
name = "github.com/opencontainers/image-spec"
packages = [
"specs-go",
"specs-go/v1"
]
revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1"
[[projects]]
name = "github.com/opencontainers/runc"
packages = [
"libcontainer/system",
"libcontainer/user"
]
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
version = "v0.1.1"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp"
]
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
[[projects]]
branch = "master"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
]
revision = "282c8707aa210456a825798969cc27edda34992a"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
version = "v1.0.4"
[[projects]]
name = "github.com/sony/gobreaker"
packages = ["."]
revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36"
version = "0.3.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[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"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish",
"ssh/terminal"
]
revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp"
]
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "88d2dcc510266da9f7f8c7f34e1940716cab5f5c"
[[projects]]
name = "gopkg.in/ory-am/dockertest.v3"
packages = ["."]
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
version = "v3.1.6"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "9df3ffee2bc61a6ce786baa7570eb61d8275197ea36560cf0cfd7cc0ea53af96"
solver-name = "gps-cdcl"
solver-version = 1
+63
View File
@@ -0,0 +1,63 @@
[[constraint]]
name = "github.com/asaskevich/govalidator"
version = "7.0.0"
[[constraint]]
branch = "master"
name = "github.com/cisco/senml"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.0.0"
[[constraint]]
name = "github.com/go-kit/kit"
version = "0.6.0"
[[constraint]]
name = "github.com/go-zoo/bone"
version = "1.2.0"
[[constraint]]
branch = "master"
name = "github.com/jinzhu/gorm"
[[constraint]]
name = "github.com/nats-io/go-nats"
version = "1.3.0"
[[constraint]]
name = "gopkg.in/ory-am/dockertest.v3"
version = "3.1.6"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
[[constraint]]
name = "github.com/satori/go.uuid"
version = "1.2.0"
[[constraint]]
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"
[prune]
go-tests = true
unused-packages = true
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://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
Copyright 2015 Mainflux
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
https://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.
+45
View File
@@ -0,0 +1,45 @@
# Mainflux follows the timeless, highly efficient and totally unfair system
# known as [Benevolent dictator for
# life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
# Drasko DRASKOVIC in the role of BDFL.
[bdfl]
[[drasko]]
Name = "Drasko DRASKOVIC"
Email = "drasko.draskovic@mainflux.com"
GitHub = "drasko"
# However, this role serves only in dead-lock events, or in a special and very rare cases
# when BDFL completely disagrees with the decisions made.
# In the normal flow of events, decisions on the project design are made through discussions,
# most often on the Pull Requests.
#
# Maintainers have the special role in the project in managing and accepting PRs,
# overall leading the project and making design decisions on the maintained subsystems.
#
# A reference list of all maintainers of the Mainflux project.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[maintainers]
[[manuel]]
Name = "Manuel IMPERIALE"
Email = "manuel.imperiale@gmail.com"
GitHub = "manuIO"
[[janko]]
Name = "Janko ISIDOROVIC"
Email = "janko.isidorovic@mainflux.com"
GitHub = "janko-isidorovic"
[[nikola]]
Name = "Nikola MARCETIC"
Email = "nikola.marcetic@mainflux.com"
GitHub = "nmarcetic"
[[dejan]]
Name = "Dejan MIJIC"
Email = "dejan.mijic@mainflux.com"
GitHub = "mijicd"
+48
View File
@@ -0,0 +1,48 @@
BUILD_DIR=build
SERVICES=manager http normalizer coap
DOCKERS=$(addprefix docker_,$(SERVICES))
all: $(SERVICES)
.PHONY: all $(SERVICES) docker
define compile_service
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,$(@))
http:
$(call compile_service,$(@))
normalizer:
$(call compile_service,$(@))
coap:
$(call compile_service,$(@))
clean:
rm -rf ${BUILD_DIR}
install:
cp ${BUILD_DIR}/* $(GOBIN)
# Docker
docker_manager:
$(call make_docker,$(@))
docker_http:
$(call make_docker,$(@))
docker_normalizer:
$(call make_docker,$(@))
docker_coap:
$(call make_docker,$(@))
docker: $(DOCKERS)
+65
View File
@@ -0,0 +1,65 @@
# Mainflux
[![build][ci-badge]][ci-url]
[![go report card][grc-badge]][grc-url]
[![license][license]](LICENSE)
[![chat][gitter-badge]][gitter]
![banner][banner]
Mainflux is modern, scalable, secure open source and patent-free IoT cloud platform written in Go.
It accepts user, device, and application connections over various network protocols (i.e. HTTP,
MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware
for building complex IoT solutions.
For more details, check out the [official documentation][docs].
## Features
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
- Device management and provisioning
- Fine-grained access control
- Platform logging and instrumentation support
- Container-based deployment using [Docker][docker]
## Quickstart
Before proceeding, install the following prerequisites:
- [Docker](https://docs.docker.com/install/)
- [Docker compose](https://docs.docker.com/compose/install/)
Once everything is installed, execute the following commands from project root:
```bash
cd docker/
docker-compose up -d
```
## Contributing
Thank you for your interest in Mainflux and wish to contribute!
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues).
2. Checkout the [contribution guide](.github/CONTRIBUTING.md) to learn more about our style and conventions.
3. Make your changes compatible to our workflow.
## Community
- [Google group][forum]
- [Gitter][gitter]
- [Twitter][twitter]
[banner]: https://github.com/mainflux/doc/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
[docker]: https://www.docker.com
[forum]: https://groups.google.com/forum/#!forum/mainflux
[gitter]: https://gitter.im/mainflux/mainflux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
[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
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
[twitter]: https://twitter.com/mainflux
-508
View File
@@ -1,508 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala Alarms API
description: |
HTTP API for managing alarms service.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.5
servers:
- url: http://localhost:8050
- url: https://localhost:8050
tags:
- name: alarms
description: Everything about your Alarms
externalDocs:
description: Find out more about alarms
url: https://magistrala.absmach.eu/docs/
paths:
/{domainID}/alarms:
get:
operationId: listAlarms
summary: List Alarms
description: |
Retrieves a list of alarms with optional filtering
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Order'
- $ref: '#/components/parameters/Dir'
- $ref: '#/components/parameters/ChannelID'
- $ref: '#/components/parameters/ClientID'
- $ref: '#/components/parameters/Subtopic'
- $ref: '#/components/parameters/RuleID'
- $ref: '#/components/parameters/Status'
- $ref: '#/components/parameters/AssigneeID'
- $ref: '#/components/parameters/Severity'
- $ref: '#/components/parameters/UpdatedBy'
- $ref: '#/components/parameters/AssignedBy'
- $ref: '#/components/parameters/AcknowledgedBy'
- $ref: '#/components/parameters/ResolvedBy'
- $ref: '#/components/parameters/CreatedFrom'
- $ref: '#/components/parameters/CreatedTo'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/AlarmsPageRes'
'400':
description: Failed due to malformed query parameters
'401':
description: Missing or invalid access token
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/alarms/{alarmID}:
get:
operationId: viewAlarm
summary: View Alarm
description: Retrieves an alarm by ID
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/AlarmRes'
'400':
description: Missing or invalid alarm ID
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
put:
operationId: updateAlarm
summary: Update Alarm
description: Updates an existing alarm
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
requestBody:
$ref: '#/components/requestBodies/AlarmUpdateReq'
responses:
'200':
$ref: '#/components/responses/AlarmRes'
'400':
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'415':
description: Missing or invalid content type
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
delete:
operationId: deleteAlarm
summary: Delete Alarm
description: Deletes an alarm
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
responses:
'204':
description: Alarm deleted successfully
'400':
description: Failed due to malformed alarm ID
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
/health:
get:
summary: Retrieves service health check info
tags:
- health
security: []
responses:
'200':
$ref: '#/components/responses/HealthRes'
'500':
$ref: '#/components/responses/ServiceError'
components:
schemas:
Alarm:
type: object
properties:
id:
type: string
description: Unique alarm identifier
readOnly: true
rule_id:
type: string
description: Rule ID that triggered this alarm
domain_id:
type: string
description: Domain ID this alarm belongs to
channel_id:
type: string
description: Channel ID where the alarm was triggered
client_id:
type: string
description: Client ID that triggered the alarm
subtopic:
type: string
description: Subtopic associated with the alarm
status:
type: string
description: Alarm status
enum: [active, cleared]
measurement:
type: string
description: Measurement that triggered the alarm
value:
type: string
description: Value that triggered the alarm
unit:
type: string
description: Unit of measurement
threshold:
type: string
description: Threshold value that was exceeded
cause:
type: string
description: Cause or description of the alarm
severity:
type: integer
description: Severity level (0-100)
minimum: 0
maximum: 100
assignee_id:
type: string
description: ID of the user assigned to this alarm
created_at:
type: string
format: date-time
description: Creation timestamp
readOnly: true
updated_at:
type: string
format: date-time
description: Last update timestamp
readOnly: true
updated_by:
type: string
description: User who last updated the alarm
readOnly: true
assigned_at:
type: string
format: date-time
description: When the alarm was assigned
readOnly: true
assigned_by:
type: string
description: User who assigned the alarm
readOnly: true
acknowledged_at:
type: string
format: date-time
description: When the alarm was acknowledged
readOnly: true
acknowledged_by:
type: string
description: User who acknowledged the alarm
readOnly: true
resolved_at:
type: string
format: date-time
description: When the alarm was resolved
readOnly: true
resolved_by:
type: string
description: User who resolved the alarm
readOnly: true
metadata:
type: object
description: Custom metadata
additionalProperties: true
AlarmsPage:
type: object
properties:
offset:
type: integer
description: Number of items to skip during retrieval
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve
minimum: 1
maximum: 1000
default: 10
total:
type: integer
description: Total number of results
minimum: 0
alarms:
type: array
minItems: 0
items:
$ref: '#/components/schemas/Alarm'
required:
- alarms
- total
- offset
- limit
parameters:
DomainID:
name: domainID
description: Domain ID
in: path
required: true
schema:
type: string
AlarmID:
name: alarmID
description: Alarm ID
in: path
required: true
schema:
type: string
Offset:
name: offset
description: Number of items to skip
in: query
required: false
schema:
type: integer
default: 0
minimum: 0
Limit:
name: limit
description: Size of the subset to retrieve
in: query
required: false
schema:
type: integer
default: 10
minimum: 1
maximum: 1000
Order:
name: order
description: Order by field
in: query
required: false
schema:
type: string
enum: [created_at, updated_at]
default: created_at
Dir:
name: dir
description: Sort direction
in: query
required: false
schema:
type: string
enum: [asc, desc]
default: desc
ChannelID:
name: channel_id
description: Filter by channel ID
in: query
required: false
schema:
type: string
ClientID:
name: client_id
description: Filter by client ID
in: query
required: false
schema:
type: string
Subtopic:
name: subtopic
description: Filter by subtopic
in: query
required: false
schema:
type: string
RuleID:
name: rule_id
description: Filter by rule ID
in: query
required: false
schema:
type: string
Status:
name: status
description: Filter by alarm status
in: query
required: false
schema:
type: string
enum: [active, cleared, all]
default: all
AssigneeID:
name: assignee_id
description: Filter by assignee ID
in: query
required: false
schema:
type: string
Severity:
name: severity
description: Filter by severity level
in: query
required: false
schema:
type: integer
minimum: 0
maximum: 100
UpdatedBy:
name: updated_by
description: Filter by user who updated
in: query
required: false
schema:
type: string
AssignedBy:
name: assigned_by
description: Filter by user who assigned
in: query
required: false
schema:
type: string
AcknowledgedBy:
name: acknowledged_by
description: Filter by user who acknowledged
in: query
required: false
schema:
type: string
ResolvedBy:
name: resolved_by
description: Filter by user who resolved
in: query
required: false
schema:
type: string
CreatedFrom:
name: created_from
description: Filter alarms created after this time (RFC3339 format)
in: query
required: false
schema:
type: string
format: date-time
CreatedTo:
name: created_to
description: Filter alarms created before this time (RFC3339 format)
in: query
required: false
schema:
type: string
format: date-time
requestBodies:
AlarmUpdateReq:
description: JSON-formatted document describing the alarm update
required: true
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: Alarm status
enum: [active, cleared]
assignee_id:
type: string
description: ID of the user assigned to this alarm
severity:
type: integer
description: Severity level (0-100)
minimum: 0
maximum: 100
metadata:
type: object
description: Custom metadata
additionalProperties: true
responses:
AlarmRes:
description: Alarm data retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/Alarm'
links:
update:
operationId: updateAlarm
parameters:
alarmID: $response.body#/id
domainID: $response.body#/domain_id
delete:
operationId: deleteAlarm
parameters:
alarmID: $response.body#/id
domainID: $response.body#/domain_id
AlarmsPageRes:
description: Alarms page retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/AlarmsPage'
ServiceError:
description: Unexpected server-side error occurred
HealthRes:
description: Service Health Check
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
-851
View File
@@ -1,851 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Magistrala Auth Service
description: |
This is the Auth Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform users. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.0
servers:
- url: http://localhost:9001
- url: https://localhost:9001
tags:
- name: Keys
description: Everything about your Keys.
externalDocs:
description: Find out more about keys
url: https://magistrala.absmach.eu/docs/
- name: PATs
description: Everything about your Personal Access Tokens.
externalDocs:
description: Find out more about Personal Access Tokens
url: https://magistrala.absmach.eu/docs/
- name: Health
description: Service health check endpoint.
externalDocs:
description: Find out more about health check
url: https://magistrala.absmach.eu/docs/
paths:
/keys:
post:
operationId: issueKey
tags:
- Keys
summary: Issue API key
description: |
Generates a new API key. Thew new API key will
be uniquely identified by its ID.
requestBody:
$ref: "#/components/requestBodies/KeyRequest"
responses:
"201":
description: Issued new key.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"409":
description: Failed due to using already existing ID.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/keys/{keyID}:
get:
operationId: getKey
summary: Gets API key details.
description: |
Gets API key details for the given key.
tags:
- Keys
parameters:
- $ref: "#/components/parameters/ApiKeyId"
responses:
"200":
$ref: "#/components/responses/KeyRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: revokeKey
summary: Revoke API key
description: |
Revoke API key identified by the given ID.
tags:
- Keys
parameters:
- $ref: "#/components/parameters/ApiKeyId"
responses:
"204":
description: Key revoked.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/pats:
post:
operationId: createPAT
tags:
- PATs
summary: Create a new Personal Access Token
description: |
Creates a new Personal Access Token (PAT) for the authenticated user.
requestBody:
$ref: "#/components/requestBodies/CreatePATRequest"
responses:
"201":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: listPATs
tags:
- PATs
summary: List all Personal Access Tokens
description: |
Lists all Personal Access Tokens (PATs) for the authenticated user.
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/PATsPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllPATs
tags:
- PATs
summary: Remove all Personal Access Tokens
description: |
Removes all Personal Access Tokens (PATs) for the authenticated user.
responses:
"200":
description: All PATs removed successfully.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}:
get:
operationId: retrievePAT
tags:
- PATs
summary: Retrieve a Personal Access Token
description: |
Retrieves details of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"200":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: deletePAT
tags:
- PATs
summary: Delete a Personal Access Token
description: |
Deletes a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"204":
description: PAT deleted successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/name:
patch:
operationId: updatePATName
tags:
- PATs
summary: Update Personal Access Token name
description: |
Updates the name of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/UpdatePATNameRequest"
responses:
"202":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/description:
patch:
operationId: updatePATDescription
tags:
- PATs
summary: Update Personal Access Token description
description: |
Updates the description of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/UpdatePATDescriptionRequest"
responses:
"202":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/reset:
patch:
operationId: resetPATSecret
tags:
- PATs
summary: Reset Personal Access Token secret
description: |
Resets the secret of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/ResetPATSecretRequest"
responses:
"200":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/revoke:
patch:
operationId: revokePATSecret
tags:
- PATs
summary: Revoke Personal Access Token secret
description: |
Revokes the secret of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"204":
description: PAT secret revoked successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope:
get:
operationId: listScopes
tags:
- PATs
summary: List scopes for a Personal Access Token
description: |
Lists all scopes for a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/ScopesPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllScopes
tags:
- PATs
summary: Remove all scopes from a Personal Access Token
description: |
Removes all scopes from a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"200":
description: All scopes removed successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/add:
patch:
operationId: addScope
tags:
- PATs
summary: Add scope to a Personal Access Token
description: |
Adds a scope to a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/AddScopeRequest"
responses:
"200":
description: Scope added successfully.
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"422":
description: Database cannot process the request.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/remove:
patch:
operationId: removeScope
tags:
- PATs
summary: Remove scope from a Personal Access Token
description: |
Removes a scope from a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/RemoveScopeRequest"
responses:
"200":
description: Scope removed successfully.
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- Health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
PAT:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: Personal Access Token unique identifier
user_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: User ID of the PAT owner
name:
type: string
example: "My PAT"
description: Name of the Personal Access Token
description:
type: string
example: "Token for automation"
description: Description of the Personal Access Token
secret:
type: string
example: "pat_1234567890abcdef"
description: Secret value of the Personal Access Token
issued_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was issued
expires_at:
type: string
format: date-time
example: "2020-11-26T13:31:52Z"
description: Time when the PAT expires
updated_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was last updated
last_used_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was last used
revoked:
type: boolean
example: false
description: Whether the PAT is revoked
revoked_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was revoked
PATsPage:
type: object
properties:
total:
type: integer
example: 10
description: Total number of PATs
offset:
type: integer
example: 0
description: Number of items to skip during retrieval
limit:
type: integer
example: 10
description: Size of the subset to retrieve
pats:
type: array
items:
$ref: "#/components/schemas/PAT"
description: List of Personal Access Tokens
Scope:
type: object
properties:
optional_domain_id:
type: string
format: uuid
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
description: Optional domain ID for the scope
entity_type:
type: string
enum:
[groups, channels, clients, domains, users, dashboards, messages]
example: "groups"
description: Type of entity the scope applies to
entity_id:
type: string
example: "*"
description: ID of the entity the scope applies to. '*' means all entities of the specified type.
operation:
type: string
enum:
[
create,
read,
list,
update,
delete,
share,
unshare,
publish,
subscribe,
]
example: "read"
description: Operation allowed by this scope
ScopesPage:
type: object
properties:
total:
type: integer
example: 10
description: Total number of scopes
offset:
type: integer
example: 0
description: Number of items to skip during retrieval
limit:
type: integer
example: 10
description: Size of the subset to retrieve
scopes:
type: array
items:
$ref: "#/components/schemas/Scope"
description: List of scopes
Key:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: API key unique identifier
issuer_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: In ID of the entity that issued the token.
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
subject:
type: string
format: string
example: "test@example.com"
description: User's email or service identifier of API key subject.
issued_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the key is generated.
expires_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the Key expires. If this field is missing,
that means that Key is valid indefinitely.
parameters:
PatID:
name: patID
description: Personal Access Token ID.
in: path
schema:
type: string
format: uuid
required: true
DomainID:
name: domainID
description: Unique domain identifier.
in: path
schema:
type: string
format: uuid
required: true
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
Status:
name: status
description: Domain status.
in: query
schema:
type: string
default: enabled
required: false
example: enabled
DomainName:
name: name
description: Domain's name.
in: query
schema:
type: string
required: false
example: "domainName"
Permission:
name: permission
description: permission.
in: query
schema:
type: string
required: false
example: "edit"
ApiKeyId:
name: keyID
description: API Key ID.
in: path
schema:
type: string
format: uuid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Metadata:
name: metadata
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
in: query
required: false
schema:
type: object
additionalProperties: {}
Type:
name: type
description: The type of the API Key.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Subject:
name: subject
description: The subject of an API Key
in: query
schema:
type: string
required: false
requestBodies:
CreatePATRequest:
description: JSON-formatted document describing PAT creation request.
required: true
content:
application/json:
schema:
type: object
required:
- name
- duration
properties:
name:
type: string
minLength: 1
example: "My PAT"
description: Name of the Personal Access Token
description:
type: string
example: "Token for automation"
description: Description of the Personal Access Token
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the PAT is valid. Format is a duration string (e.g. "30d", "24h", "1y").
UpdatePATNameRequest:
description: JSON-formatted document describing PAT name update request.
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
example: "New PAT Name"
description: New name for the Personal Access Token
UpdatePATDescriptionRequest:
description: JSON-formatted document describing PAT description update request.
required: true
content:
application/json:
schema:
type: object
required:
- description
properties:
description:
type: string
example: "New PAT Description"
description: New description for the Personal Access Token
ResetPATSecretRequest:
description: JSON-formatted document describing PAT secret reset request.
required: true
content:
application/json:
schema:
type: object
required:
- duration
properties:
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the new PAT secret is valid. Format is a duration string (e.g. "30d", "24h", "1y").
AddScopeRequest:
description: JSON-formatted document describing add scope request.
required: true
content:
application/json:
schema:
type: object
required:
- scopes
properties:
scopes:
type: array
items:
$ref: "#/components/schemas/Scope"
description: List of scopes to add
RemoveScopeRequest:
description: JSON-formatted document describing remove scope request.
required: true
content:
application/json:
schema:
type: object
required:
- scopes_id
properties:
scopes_id:
type: array
items:
type: string
format: uuid
description: List of scope IDs to remove
KeyRequest:
description: JSON-formatted document describing key request.
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
duration:
type: number
format: integer
example: 23456
description: Number of seconds issued token is valid for.
responses:
PATRes:
description: Personal Access Token data.
content:
application/json:
schema:
$ref: "#/components/schemas/PAT"
PATsPageRes:
description: Page of Personal Access Tokens.
content:
application/json:
schema:
$ref: "#/components/schemas/PATsPage"
ScopesPageRes:
description: Page of scopes.
content:
application/json:
schema:
$ref: "#/components/schemas/ScopesPage"
ServiceError:
description: Unexpected server-side error occurred.
KeyRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Key"
links:
revoke:
operationId: revokeKey
parameters:
keyID: $response.body#/id
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
security:
- bearerAuth: []
-1151
View File
File diff suppressed because it is too large Load Diff
-722
View File
@@ -1,722 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Certs Service API
description: |
Certificate management service for issuing, renewing, revoking, and managing X.509 certificates.
This service provides PKI functionality including certificate lifecycle management, OCSP responder,
and CRL generation.
version: 1.0.0
contact:
name: Abstract Machines
license:
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://localhost:9019
description: Development server
tags:
- name: certificates
description: Certificate lifecycle management operations
- name: pki
description: PKI infrastructure operations (OCSP, CRL, CA)
- name: health
description: Service health and monitoring
security:
- BearerAuth: []
paths:
/{domainID}/certs/issue/{entityID}:
post:
tags:
- certificates
summary: Issue a new certificate
description: Issues a new X.509 certificate for the specified entity with custom subject options
operationId: issueCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueCertRequest'
responses:
'201':
description: Certificate successfully issued
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}/renew:
patch:
tags:
- certificates
summary: Renew a certificate
description: Renews an existing certificate with extended TTL and new serial number
operationId: renewCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'200':
description: Certificate successfully renewed
content:
application/json:
schema:
$ref: '#/components/schemas/RenewCertResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}/revoke:
patch:
tags:
- certificates
summary: Revoke a certificate
description: Revokes a certificate by its serial number
operationId: revokeCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'204':
description: Certificate successfully revoked
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{entityID}/delete:
delete:
tags:
- certificates
summary: Delete certificates for an entity
description: Deletes all certificates associated with the specified entity
operationId: deleteCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
responses:
'204':
description: Certificates successfully deleted
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs:
get:
tags:
- certificates
summary: List certificates
description: Retrieves a paginated list of certificates with optional filtering by entity ID
operationId: listCerts
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/EntityIDFilter'
responses:
'200':
description: Certificates successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}:
get:
tags:
- certificates
summary: View certificate details
description: Retrieves detailed information about a specific certificate by serial number
operationId: viewCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'200':
description: Certificate details successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ViewCertResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/csrs/{entityID}:
post:
tags:
- certificates
summary: Issue certificate from CSR
description: Issues a certificate from a Certificate Signing Request (CSR)
operationId: issueFromCSR
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
- $ref: '#/components/parameters/TTL'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRRequest'
responses:
'200':
description: Certificate successfully issued from CSR
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/csrs/{entityID}:
post:
tags:
- certificates
summary: Issue certificate from CSR (Internal)
description: Issues a certificate from a CSR using internal agent authentication
operationId: issueFromCSRInternal
security:
- AgentAuth: []
parameters:
- $ref: '#/components/parameters/EntityID'
- $ref: '#/components/parameters/TTL'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRRequest'
responses:
'200':
description: Certificate successfully issued from CSR
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/ocsp:
post:
tags:
- pki
summary: OCSP responder
description: |
Online Certificate Status Protocol (OCSP) responder endpoint.
Accepts both binary OCSP requests and JSON format requests.
operationId: ocsp
security: []
parameters:
- name: force_status
in: query
description: Force a specific OCSP status for testing
required: false
schema:
type: string
requestBody:
required: true
content:
application/ocsp-request:
schema:
type: string
format: binary
description: DER-encoded OCSP request
application/json:
schema:
$ref: '#/components/schemas/OCSPRequest'
responses:
'200':
description: OCSP response
content:
application/ocsp-response:
schema:
type: string
format: binary
description: DER-encoded OCSP response
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/crl:
get:
tags:
- pki
summary: Generate Certificate Revocation List
description: Generates and returns the current Certificate Revocation List (CRL)
operationId: generateCRL
security: []
responses:
'200':
description: CRL successfully generated
content:
application/json:
schema:
$ref: '#/components/schemas/CRLResponse'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/view-ca:
get:
tags:
- pki
summary: View CA certificate
description: Retrieves the CA certificate chain (root and intermediate certificates)
operationId: viewCA
security: []
responses:
'200':
description: CA certificate successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ViewCertResponse'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/download-ca:
get:
tags:
- pki
summary: Download CA certificate
description: Downloads the CA certificate as a ZIP file
operationId: downloadCA
security: []
responses:
'200':
description: CA certificate ZIP file
content:
application/zip:
schema:
type: string
format: binary
'500':
$ref: '#/components/responses/InternalServerError'
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
'200':
$ref: '#/components/responses/HealthRes'
'500':
$ref: '#/components/responses/InternalServerError'
/metrics:
get:
tags:
- health
summary: Prometheus metrics
description: Returns Prometheus metrics for monitoring
operationId: metrics
security: []
responses:
'200':
description: Metrics successfully retrieved
content:
text/plain:
schema:
type: string
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: User authentication token
AgentAuth:
type: http
scheme: bearer
description: Agent authentication token for internal operations
parameters:
DomainID:
name: domainID
in: path
required: true
description: Domain identifier
schema:
type: string
EntityID:
name: entityID
in: path
required: true
description: Entity identifier for the certificate
schema:
type: string
CertID:
name: id
in: path
required: true
description: Certificate serial number
schema:
type: string
Offset:
name: offset
in: query
description: Number of items to skip
schema:
type: integer
minimum: 0
default: 0
Limit:
name: limit
in: query
description: Maximum number of items to return
schema:
type: integer
minimum: 1
maximum: 100
default: 10
EntityIDFilter:
name: entity_id
in: query
description: Filter certificates by entity ID
schema:
type: string
TTL:
name: ttl
in: query
description: Time to live for the certificate (e.g., "8760h", "365d")
schema:
type: string
schemas:
IssueCertRequest:
type: object
required:
- options
properties:
ttl:
type: string
description: Time to live for the certificate (e.g., "8760h" for 1 year)
example: "8760h"
ip_addresses:
type: array
items:
type: string
description: IP addresses to include in the certificate
example: ["192.168.1.1", "10.0.0.1"]
options:
$ref: '#/components/schemas/SubjectOptions'
SubjectOptions:
type: object
required:
- common_name
properties:
common_name:
type: string
description: Common Name (CN) for the certificate subject
example: "example.com"
organization:
type: array
items:
type: string
description: Organization (O)
example: ["Abstract Machines"]
organizational_unit:
type: array
items:
type: string
description: Organizational Unit (OU)
example: ["Engineering"]
country:
type: array
items:
type: string
description: Country (C)
example: ["US"]
province:
type: array
items:
type: string
description: Province or State (ST)
example: ["California"]
locality:
type: array
items:
type: string
description: Locality or City (L)
example: ["San Francisco"]
street_address:
type: array
items:
type: string
description: Street Address
example: ["123 Main St"]
postal_code:
type: array
items:
type: string
description: Postal Code
example: ["94105"]
dns_names:
type: array
items:
type: string
description: DNS names for Subject Alternative Names
example: ["example.com", "www.example.com"]
ip_addresses:
type: array
items:
type: string
description: IP addresses for Subject Alternative Names
example: ["192.168.1.1"]
CertificateResponse:
type: object
properties:
serial_number:
type: string
description: Unique serial number of the certificate
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
key:
type: string
description: PEM-encoded private key
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
revoked:
type: boolean
description: Whether the certificate is revoked
example: false
expiry_time:
type: string
format: date-time
description: Certificate expiration time
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Entity identifier associated with the certificate
example: "entity-123"
RenewCertResponse:
type: object
properties:
certificate:
$ref: '#/components/schemas/ViewCertResponse'
ViewCertResponse:
type: object
properties:
serial_number:
type: string
description: Certificate serial number
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
key:
type: string
description: PEM-encoded private key
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
revoked:
type: boolean
description: Revocation status
example: false
expiry_time:
type: string
format: date-time
description: Expiration timestamp
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Associated entity identifier
example: "entity-123"
CertificateListResponse:
type: object
properties:
total:
type: integer
format: uint64
description: Total number of certificates
example: 100
offset:
type: integer
format: uint64
description: Current offset
example: 0
limit:
type: integer
format: uint64
description: Current limit
example: 10
certificates:
type: array
items:
$ref: '#/components/schemas/ViewCertResponse'
IssueFromCSRRequest:
type: object
required:
- csr
properties:
csr:
type: string
format: byte
description: PEM-encoded Certificate Signing Request
example: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K..."
IssueFromCSRResponse:
type: object
properties:
serial_number:
type: string
description: Serial number of the issued certificate
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
revoked:
type: boolean
description: Revocation status
example: false
expiry_time:
type: string
format: date-time
description: Expiration timestamp
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Associated entity identifier
example: "entity-123"
OCSPRequest:
type: object
properties:
serial_number:
type: string
description: Certificate serial number to check
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate to check
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
status:
type: string
description: Force a specific status (for testing)
enum: [good, revoked, unknown]
CRLResponse:
type: object
properties:
crl:
type: string
format: byte
description: DER-encoded Certificate Revocation List
Error:
type: object
properties:
error:
type: string
description: Error message
example: "invalid request"
responses:
BadRequest:
description: Bad request - invalid parameters or malformed request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized - invalid or missing authentication token
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
UnprocessableEntity:
description: Unprocessable entity - request cannot be processed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: './schemas/health_info.yaml'
-1149
View File
File diff suppressed because it is too large Load Diff
-1585
View File
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
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"
)
type config struct {
Port int
NatsURL string
}
func main() {
cfg := loadConfig()
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
nc := connectToNats(cfg, logger)
defer nc.Close()
pub := nats.NewMessagePublisher(nc)
ca := adapter.NewCoAPAdapter(logger, pub)
nc.Subscribe("src.http", ca.BridgeHandler)
nc.Subscribe("src.mqtt", ca.BridgeHandler)
errs := make(chan error, 2)
go func() {
coapAddr := fmt.Sprintf(":%d", cfg.Port)
errs <- ca.Serve(coapAddr)
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
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
}
+88
View File
@@ -0,0 +1,88 @@
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"
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"
broker "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defPort string = "8180"
defNatsURL string = broker.DefaultURL
defManagerURL string = "http://localhost:8180"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envManagerURL string = "MF_MANAGER_URL"
)
type config struct {
ManagerURL string
NatsURL string
Port string
}
func main() {
cfg := config{
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Log("error", err)
os.Exit(1)
}
defer nc.Close()
pub := nats.NewMessagePublisher(nc)
svc := adapter.New(pub)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "http_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "http_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)
mc := manager.NewClient(cfg.ManagerURL)
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
logger.Log("terminated", <-errs)
}
+107
View File
@@ -0,0 +1,107 @@
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)
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/normalizer"
nats "github.com/nats-io/go-nats"
)
const (
defNatsURL string = nats.DefaultURL
defPort string = "8180"
envNatsURL string = "MF_NATS_URL"
envPort string = "MF_NORMALIZER_PORT"
)
type config struct {
NatsURL string
Port string
}
func main() {
cfg := config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Log("error", fmt.Sprintf("Failed to connect: %s", err))
os.Exit(1)
}
defer nc.Close()
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
normalizer.Subscribe(nc, logger)
logger.Log("terminated", <-errs)
}
+52
View File
@@ -0,0 +1,52 @@
# Mainflux CoAP Adapter
Mainflux CoAP adapter provides an [CoAP](http://coap.technology/) API for sending messages through the
platform.
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|-----------------------|-------------------|-----------------------|
| COAP_ADAPTER_NATS_URL | NATS instance URL | nats://localhost:4222 |
## Deployment
The service is distributed as Docker container. The following snippet provides
a compose file template that can be used to deploy the service container locally:
```yaml
version: "2"
services:
adapter:
image: mainflux/coap-adapter:[version]
container_name: [instance name]
ports:
- [host machine port]:5683
environment:
COAP_ADAPTER_NATS_URL: [NATS instance URL]
```
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
# 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
# set the environment variables and run the service
COAP_ADAPTER_NATS_URL=[NATS instance URL] app
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
+61
View File
@@ -0,0 +1,61 @@
package adapter
import (
"encoding/json"
"log"
"net"
"github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
const protocol string = "coap"
type Observer struct {
conn *net.UDPConn
addr *net.UDPAddr
message *coap.Message
}
type CoAPAdapter struct {
obsMap map[string][]Observer
logger *zap.Logger
pub mainflux.MessagePublisher
}
// 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),
}
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())
}
// 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))
// 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
}
}
log.Println("Calling obsTransmit()")
log.Println(m.Publisher, m.Protocol, m.Channel, m.Payload)
ca.obsTransmit(m)
}
+145
View File
@@ -0,0 +1,145 @@
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
}
}
}
+31
View File
@@ -0,0 +1,31 @@
// 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)
}
+30
View File
@@ -0,0 +1,30 @@
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
}
+3
View File
@@ -0,0 +1,3 @@
// Package mainflux acts as an umbrella package containing multiple different
// microservices and defines all shared domain concepts.
package mainflux
+3
View File
@@ -0,0 +1,3 @@
docker-compose.yml
nginx.conf
ssl/
+11
View File
@@ -0,0 +1,11 @@
FROM golang:1.9-alpine AS builder
ARG SVC_NAME
RUN apk update \
&& apk add git \
&& go get github.com/mainflux/mainflux \
&& cd /go/src/github.com/mainflux/mainflux/cmd/$SVC_NAME \
&& CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o /exe
FROM scratch
COPY --from=builder /exe /
ENTRYPOINT ["/exe"]
+78
View File
@@ -0,0 +1,78 @@
###
# Copyright (c) 2015-2017 Mainflux
#
# Mainflux is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
version: "3"
services:
nginx:
image: nginx:1.13-alpine
container_name: mainflux-nginx
network_mode: bridge
volumes:
- $PWD/nginx.conf:/etc/nginx/nginx.conf
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
ports:
- "80:80"
- "443:443"
- "8883:8883"
nats:
image: nats:1.0.2
container_name: mainflux-nats
network_mode: bridge
postgres:
image: postgres:10.2-alpine
container_name: mainflux-postgres
network_mode: bridge
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: mainflux
manager:
image: mainflux/manager:latest
container_name: mainflux-manager
network_mode: bridge
environment:
MF_DB_HOST: postgres
MF_MANAGER_DB: mainflux
MF_MANAGER_PORT: 8180
MF_MANAGER_SECRET: test-secret
normalizer:
image: mainflux/normalizer:latest
container_name: mainflux-normalizer
network_mode: bridge
environment:
MF_NATS_URL: "nats://nats:4222"
MF_NORMALIZER_PORT: 8181
http-adapter:
image: mainflux/http:latest
container_name: mainflux-http
network_mode: bridge
depends_on:
- manager
environment:
MF_MANAGER_URL: "http://manager:8180"
MF_NATS_URL: "nats://nats:4222"
MF_HTTP_ADAPTER_PORT: 8182
mqtt-adapter:
image: mainflux/mqtt-adapter:latest
container_name: mainflux-mqtt
network_mode: bridge
depends_on:
- manager
environment:
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
AUTH_URL: "http://manager"
AUTH_PORT: 8180
+186
View File
@@ -0,0 +1,186 @@
###
# Mainflux NGINX Conf
#
# Taken for /etc/nginx/nginx.conf on Debian machine
# and https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/nginx.conf
###
##
# User:
# - 'www-data' on Debian
# - 'nginx' on Alpine
##
#user www-data;
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
###
# HTTP
###
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
upstream docker-manager {
server mainflux-manager:8180;
}
upstream docker-http {
server mainflux-http:8182;
}
##
# Virtual Host Configs
##
# HTTP
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
return 302 https://$server_name$request_uri;
}
# HTTPS
server {
# SSL configuration
#
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
# Certificates
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
ssl_stapling off;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable preloading HSTS for now. You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers "*";
server_name localhost;
# Proxy pass to manager service
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://docker-manager/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location ~ ^/channels/[a-zA-Z0-9-]+/messages$ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://docker-http/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
}
###
# MQTT
###
stream {
upstream docker-mqtt {
server mainflux-mqtt:1883;
}
server {
listen 8883 ssl;
proxy_pass docker-mqtt;
# Certificates
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
}
}
+30
View File
@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFFTCCA/2gAwIBAgIJAPtBp1R03oQDMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTcwMTA3MDA1NTIw
WhcNMzIwMTA0MDA1NTIwWjBdMRgwFgYDVQQDDA9tYWluZmx1eC1zZXJ2ZXIxETAP
BgNVBAoMCE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWlu
Zm9AbWFpbmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
mGShbFfHhE/Q/CvVgpfas9o2B2H4E1nrDSmQXqwUPJ2U5+ExqpPkM/xX77yC9gut
k0RrM3fhB5AET9GnGMyFFlNYNPnWZOS6iQN2u5dg02TRqKdIhZZhs438u2wSRVwg
oAqYyiVpoV3QFa2BWTYbWwAhQGPXMUmUN8ZaUcWJ+s8PNhisj458hbWl7HuJ3ICB
xEAXiyd529YVHYJZVHyyEF5GwIvw3DOzZ++Ip6IVd0zYbdHw1pV5SAI5fbc4cb0C
cKXkSlqtjmz6ZeUWLJV98rYHc0YhUR7y2a6zNNFS/ROU6KEsWIdDTNDph0nHS1dB
sj4Abj2Kmf9InIPHHvqF+wIDAQABo4IB3DCCAdgwDAYDVR0TAQH/BAIwADARBglg
hkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgXgMCoGCWCGSAGG+EIBDQQdFhtNYWlu
Zmx1eCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFB7OnWLMd24Eo5dcmJss
lLMkq59vMIGIBgNVHSMEgYAwfoAUiBGMTsVJvNuSKvwoi/7aEvGfRX2hW6RZMFcx
EjAQBgNVBAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsM
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb22CCQDzlkP5P+dv
3TBEBgNVHREEPTA7hwTAqAAuhxD+gAAAAAAAAGJsZv/+yw+7hwR/AAABhxAAAAAA
AAAAAAAAAAAAAAABgglsb2NhbGhvc3QwgYsGA1UdIASBgzCBgDB+BgMrBQgwdzAc
BggrBgEFBQcCARYQaHR0cDovL2xvY2FsaG9zdDBXBggrBgEFBQcCAjBLMA8WCE1h
aW5mbHV4MAMCAQEaOFRoaXMgQ0EgaXMgZm9yIGEgbG9jYWwgTWFpbmZsdXggc2Vy
dmVyIGluc3RhbGxhdGlvbiBvbmx5MA0GCSqGSIb3DQEBDQUAA4IBAQBxGCqJRbnw
lKHhpqZVEEWV1t87wQnf2qOV8SOzh1evF5sYeYEOnb2d802r0p08kuiJNETZdJOh
K/f7dVCL+mtSzHiK8SY8WJ8l0xfW+0qo/GW9jd9QDGbuwi6cRUw1lRhr5p/0ge9N
e3VlI7cjpG/Kv3x1AtCjMpMLzAxOLZmbSWgrMvtJIsMHcQTiV1HexIq9/A3XVthf
zuRUr1qyj3nx6ga2eHqaJQ5/Zu1A7zjHbZTiW4U5Ikl1PDWL3V0uEb3bXZ7xABb9
pjYjDA1Bm4eQMPJ+ZWRs5EFHBnLJc/Kz+4sfUuwiqI4xz2LMeCrdMbh0YSP/rhi4
wERrPpFmvpN3
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmGShbFfHhE/Q/CvVgpfas9o2B2H4E1nrDSmQXqwUPJ2U5+Ex
qpPkM/xX77yC9gutk0RrM3fhB5AET9GnGMyFFlNYNPnWZOS6iQN2u5dg02TRqKdI
hZZhs438u2wSRVwgoAqYyiVpoV3QFa2BWTYbWwAhQGPXMUmUN8ZaUcWJ+s8PNhis
j458hbWl7HuJ3ICBxEAXiyd529YVHYJZVHyyEF5GwIvw3DOzZ++Ip6IVd0zYbdHw
1pV5SAI5fbc4cb0CcKXkSlqtjmz6ZeUWLJV98rYHc0YhUR7y2a6zNNFS/ROU6KEs
WIdDTNDph0nHS1dBsj4Abj2Kmf9InIPHHvqF+wIDAQABAoIBAGyneyyrXXbqDcBu
ZHoLWYTYdaNH57+sYdnto6DMolUhqdS2jFnpvlCOgAhPaTSS2PxiUOjOdWSV+20J
t1EIKW/klsSWyZUAPDuKe7J+2St/+7h7JUsSELEb8HGVOWW4rQ5O3+dpS2ohYEbE
gbAg0tpMOmkVho3+vy4RP76D0MBAnhgl99fjo9jpNxPmBis+L/IJS5SNO1JFXMCf
rvyOIekmtmHBI4PgubVylwOIt03r867gi6WpSOBq5rTusDqwCdmDCeRWDL4EP3Zr
VH8EPbS1Zlcw0FH8yXjp2ts5UcJZL5CdLY7jJ6vkIiw64nNWx7cR4qd55ut280K4
tx4yqnkCgYEAxkQRZgxNHu+7KMUt7gjYRLaNhiSFrcwuSVb8HlhsYYYMH3GPQhvr
dQOOiibzsCna96GLbfdOpi9iwF82uuwNpdeKptPNb5Mht4oNqu5kKg1sl6S6fjSv
yrJACC0NcLRS2LYuBcylUCKsU7g3C9X6VmaSsh415v1s2qFHFik2AycCgYEAxMTt
kHP+pIwr35IvamRCGtUvI0D9R62gxxovpfQeetQMhc/3tOBJe/GOXDP4jitrz1l1
YMcLiCqktFxJqi6UKxcTwnGgcyPyC2UZSJJ/0lOIZDSP7JM7dT1xkrGeQyMjC09C
AwRi/ZAUUwLsMRxfTrj8igF8Md+LIjKBcYdm2w0CgYEAxg6hQsvvDoR09pli9HKp
eJrUbbh2QdPCOUlHuhiizBlYauDKN0QkxlOzRJb8wHJPZyhdXJC8ZI7Zm0qCJeBB
EfZrb5QNmPPlrq+eT66tKMUYQbQxCHohUd8W0BQRZRD94ba76tcwHQlGFKvlcVFk
LoNw77X2KrXm09BgbubkKekCgYAygztZMe3U4AcDRcvWTBaMPN309uIOXIxBkH9a
4uhQL89nKpQ0Yr96ifA5yz2rgYoTmKuBRJe5RPkzM93VSk/PIAV6jSDbbgbc1f8/
mhwmKjuBPd0UpldFKZjWR7KRGZwNczNHAwFGho4xITbxBI+S7fomk2sGgpR9GuoP
8up8oQKBgEMd9Mbo3xC0xW29V5P3FUKH4zRYJYlpk+30bZ4VCrgim9cVBgN/xrIA
l4yEnitEi1591b/r+Uz3b6yWOiLEHRE2U7sQLbfh2fvF0VhFCOE11FeAuoZp89/k
TVnL/FelAMbL1iZFIf9LRY2DdfAUKlO//cXMHcC2iatBUwfylSVS
-----END RSA PRIVATE KEY-----
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAquN8NRcSdLOM9RiumqWH8Jw3CGVR/eQQeq+jvT3zpxlUQPAMExQb
MRCspm1oRgDWGvch3Z4zfMmBZyzKJA4BDTh4USzcE5zvnx8aUcUPZPQpwSicKgzb
QGnl0Xf/75GAWrwhxn8GNyMP29wrpcd1Qg8fEQ3HAW1fCd9girKMKY9aBaHli/h2
R9Rd/KTbeqN88aoMjUvZHooIIZXu0A+kyulOajYQO4k3Sp6CBqv0FFcoLQnYNH13
kMUE5qJ68U732HybTw8sofTCOxKcCfM2kVP7dVoF3prlGjUw3z3l3STY8vuTdq0B
R7PslkoQHNmqcL+2gouoWP3GI+IeRzGSSwIBAg==
-----END DH PARAMETERS-----
-1433
View File
File diff suppressed because it is too large Load Diff
-1791
View File
File diff suppressed because it is too large Load Diff
-193
View File
@@ -1,193 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala http adapter
description: |
HTTP API for sending messages through communication channels.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.0
servers:
- url: http://localhost:8008
- url: https://localhost:8008
tags:
- name: messages
description: Everything about your Messages
externalDocs:
description: Find out more about messages
url: https://magistrala.absmach.eu/docs/
paths:
/m/{domainPrefix}/c/{channelPrefix}:
post:
summary: Sends message to the communication channel
description: |
Sends message to the communication channel. Messages can be sent as
JSON formatted SenML or as blob.
tags:
- messages
parameters:
- $ref: "#/components/parameters/domainPrefix"
- $ref: "#/components/parameters/channelPrefix"
requestBody:
$ref: "#/components/requestBodies/MessageReq"
responses:
"202":
description: Message is accepted for processing.
"400":
description: Message discarded due to its malformed content.
"401":
description: Missing or invalid access token provided.
"403":
description: Access denied to the requested resource.
"404":
description: Message discarded due to invalid channel id.
"415":
description: Message discarded due to invalid or missing content type.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
SenMLRecord:
type: object
properties:
bn:
type: string
description: Base Name
bt:
type: number
format: double
description: Base Time
bu:
type: number
format: double
description: Base Unit
bv:
type: number
format: double
description: Base Value
bs:
type: number
format: double
description: Base Sum
bver:
type: number
format: double
description: Version
n:
type: string
description: Name
u:
type: string
description: Unit
v:
type: number
format: double
description: Value
vs:
type: string
description: String Value
vb:
type: boolean
description: Boolean Value
vd:
type: string
description: Data Value
s:
type: number
format: double
description: Value Sum
t:
type: number
format: double
description: Time
ut:
type: number
format: double
description: Update Time
SenMLArray:
type: array
items:
$ref: "#/components/schemas/SenMLRecord"
parameters:
domainPrefix:
name: domainPrefix
description: ID or route of the domain associated with the channel and client.
in: path
schema:
type: string
example: mydomain
required: true
channelPrefix:
name: channelPrefix
description: ID or route of the channel connected to the client.
in: path
schema:
type: string
example: mychannel
required: true
requestBodies:
MessageReq:
description: |
Message to be distributed. Since the platform expects messages to be
properly formatted SenML in order to be post-processed, clients are
obliged to specify Content-Type header for each published message.
Note that all messages that aren't SenML will be accepted and published,
but no post-processing will be applied.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SenMLArray"
responses:
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: uuid
description: |
* Client access: "Authorization: Client <client_key>"
basicAuth:
type: http
scheme: basic
description: |
* Clients access: "Authorization: Basic <base64-encoded_credentials>"
security:
- bearerAuth: []
- basicAuth: []
+54
View File
@@ -0,0 +1,54 @@
# HTTP adapter
HTTP adapter provides an HTTP API for sending messages through the platform.
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|----------------------|---------------------|-----------------------|
| MF_MANAGER_URL | Manager service URL | http://localhost:8180 |
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_HTTP_ADAPTER_PORT | Service HTTP port | 8180 |
## Deployment
The service is distributed as Docker container. The following snippet provides
a compose file template that can be used to deploy the service container locally:
```yaml
version: "2"
services:
adapter:
image: mainflux/http:[version]
container_name: [instance name]
ports:
- [host machine port]:8180
environment:
MF_MANAGER_URL: [Manager service URL]
MF_NATS_URL: [NATS instance URL]
MF_HTTP_ADAPTER_PORT: [Service HTTP port]
```
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/http
# 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
# set the environment variables and run the service
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] app
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
+18
View File
@@ -0,0 +1,18 @@
package http
import "github.com/mainflux/mainflux"
var _ mainflux.MessagePublisher = (*adapterService)(nil)
type adapterService struct {
pub mainflux.MessagePublisher
}
// New instantiates the domain service implementation.
func New(pub mainflux.MessagePublisher) mainflux.MessagePublisher {
return &adapterService{pub}
}
func (as *adapterService) Publish(msg mainflux.RawMessage) error {
return as.pub.Publish(msg)
}
+3
View File
@@ -0,0 +1,3 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
+16
View File
@@ -0,0 +1,16 @@
package api
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux"
)
func sendMessageEndpoint(svc mainflux.MessagePublisher) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
msg := request.(mainflux.RawMessage)
err := svc.Publish(msg)
return nil, err
}
}
+31
View File
@@ -0,0 +1,31 @@
package api
import (
"time"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux"
)
var _ mainflux.MessagePublisher = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc mainflux.MessagePublisher
}
// LoggingMiddleware adds logging facilities to the adapter.
func LoggingMiddleware(svc mainflux.MessagePublisher, logger log.Logger) mainflux.MessagePublisher {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) error {
defer func(begin time.Time) {
lm.logger.Log(
"method", "publish",
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Publish(msg)
}
+34
View File
@@ -0,0 +1,34 @@
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux"
)
var _ mainflux.MessagePublisher = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc mainflux.MessagePublisher
}
// MetricsMiddleware instruments adapter by tracking request count and latency.
func MetricsMiddleware(svc mainflux.MessagePublisher, counter metrics.Counter, latency metrics.Histogram) mainflux.MessagePublisher {
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)
}
+116
View File
@@ -0,0 +1,116 @@
package api
import (
"context"
"errors"
"io"
"io/ioutil"
"net/http"
"strings"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
manager "github.com/mainflux/mainflux/manager/client"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const protocol string = "http"
var (
errMalformedData error = errors.New("malformed SenML data")
errUnknownType error = errors.New("unknown content type")
errUnauthorizedAccess error = errors.New("missing or invalid credentials provided")
auth manager.ManagerClient
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc mainflux.MessagePublisher, mc manager.ManagerClient) http.Handler {
auth = mc
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
r := bone.New()
r.Post("/channels/:id/messages", kithttp.NewServer(
sendMessageEndpoint(svc),
decodeRequest,
encodeResponse,
opts...,
))
r.GetFunc("/version", mainflux.Version())
r.Handle("/metrics", promhttp.Handler())
return r
}
func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
publisher, err := authorize(r)
if err != nil {
return nil, err
}
payload, err := decodePayload(r.Body)
if err != nil {
return nil, err
}
msg := mainflux.RawMessage{
Publisher: publisher,
Protocol: protocol,
ContentType: r.Header.Get("Content-Type"),
Channel: bone.GetValue(r, "id"),
Payload: payload,
}
return msg, nil
}
func authorize(r *http.Request) (string, error) {
apiKey := r.Header.Get("Authorization")
if apiKey == "" {
return "", errUnauthorizedAccess
}
// extract ID from /channels/:id/messages
c := strings.Split(r.URL.Path, "/")[2]
id, err := auth.CanAccess(c, apiKey)
if err != nil {
return "", err
}
return id, nil
}
func decodePayload(body io.ReadCloser) ([]byte, error) {
payload, err := ioutil.ReadAll(body)
if err != nil {
return nil, errMalformedData
}
defer body.Close()
return payload, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.WriteHeader(http.StatusAccepted)
return nil
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
switch err {
case errMalformedData:
w.WriteHeader(http.StatusBadRequest)
case errUnknownType:
w.WriteHeader(http.StatusUnsupportedMediaType)
case manager.ErrUnauthorizedAccess:
w.WriteHeader(http.StatusForbidden)
default:
w.WriteHeader(http.StatusInternalServerError)
}
}
+3
View File
@@ -0,0 +1,3 @@
// Package http contains the domain concept definitions needed to support
// Mainflux http adapter service functionality.
package http
+31
View File
@@ -0,0 +1,31 @@
// Package nats contains NATS message publisher implementation.
package nats
import (
"encoding/json"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
)
const topic string = "src.http"
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)
}
+51
View File
@@ -0,0 +1,51 @@
swagger: "2.0"
info:
title: Mainflux http adapter
description: HTTP API for sending messages through communication channels.
version: "1.0.0"
paths:
/channels/{id}/messages:
post:
summary: Sends message to the communication channel
description: |
Sends message to the communication channel. Messages can be sent as
JSON formatted SenML or as blob.
tags:
- messages
consumes:
- "application/senml+json"
- "text/plain"
produces: []
parameters:
- name: Authorization
description: Access token.
in: header
type: string
required: true
- name: id
description: Unique channel identifier.
in: path
type: string
format: uuid
required: true
- name: message
description: |
Message to be distributed. Since the platform expects messages to be
properly formatted SenML in order to be post-processed, clients are
obliged to specify Content-Type header for each published message.
Note that all messages that aren't SenML will be accepted and published,
but no post-processing will be applied.
in: body
required: true
type: string
responses:
202:
description: Message is accepted for processing.
400:
description: Message discarded due to its malformed content.
403:
description: Message discarded due to missing or invalid credentials.
415:
description: Message discarded due to invalid or missing content type.
500:
description: Unexpected server-side error occured.
-181
View File
@@ -1,181 +0,0 @@
<!--
Copyright (c) Abstract Machines
SPDX-License-Identifier: Apache-2.0
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Magistrala API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui.css">
<style>
body {
margin: 0;
padding: 0;
}
.topbar {
display: none;
}
.service-selector {
background: #1b1b1b;
padding: 20px;
text-align: center;
}
.service-selector h1 {
color: #fff;
margin: 0 0 15px 0;
font-family: sans-serif;
}
.service-dropdown-container {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.service-dropdown-container label {
color: #fff;
font-family: sans-serif;
font-size: 14px;
font-weight: 500;
}
.service-dropdown {
background: #2d2d2d;
color: white;
border: 1px solid #4990e2;
padding: 10px 40px 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
min-width: 200px;
appearance: none;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="%23ffffff" d="M6 9L1 4h10z"/></svg>');
background-repeat: no-repeat;
background-position: right 12px center;
}
.service-dropdown:hover {
background-color: #3a3a3a;
border-color: #357abd;
}
.service-dropdown:focus {
outline: none;
border-color: #4990e2;
box-shadow: 0 0 0 2px rgba(73, 144, 226, 0.3);
}
/* Responsive styles for mobile */
@media (max-width: 768px) {
.service-selector {
padding: 15px 10px;
}
.service-selector h1 {
font-size: 1.5rem;
margin: 0 0 12px 0;
}
.service-dropdown-container {
flex-direction: column;
gap: 8px;
}
.service-dropdown-container label {
font-size: 13px;
}
.service-dropdown {
width: 100%;
max-width: 300px;
min-width: auto;
font-size: 13px;
padding: 8px 35px 8px 12px;
}
}
@media (max-width: 480px) {
.service-selector h1 {
font-size: 1.25rem;
}
.service-dropdown {
max-width: 250px;
}
}
</style>
</head>
<body>
<div class="service-selector">
<h1>Magistrala API Documentation</h1>
<div class="service-dropdown-container">
<label for="serviceDropdown">Select Service:</label>
<select id="serviceDropdown" class="service-dropdown"></select>
</div>
</div>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-standalone-preset.js"></script>
<script>
// Available API specifications
const APIs = ["alarms.yaml","auth.yaml","bootstrap.yaml","certs.yaml","channels.yaml","clients.yaml","domains.yaml","groups.yaml","http.yaml","notifiers.yaml","readers.yaml","reports.yaml","rules.yaml","users.yaml"];
// Get the service from URL query parameter, default to first service
function getServiceFromURL() {
const params = new URLSearchParams(window.location.search);
const service = params.get('service');
return service && APIs.includes(service) ? service : APIs[0];
}
// Update URL with selected service
function updateURL(service) {
const url = new URL(window.location);
url.searchParams.set('service', service);
window.history.pushState({}, '', url);
}
// Create service selector dropdown
function createServiceDropdown() {
const dropdown = document.getElementById('serviceDropdown');
const currentService = getServiceFromURL();
APIs.forEach(api => {
const option = document.createElement('option');
option.value = api;
let serviceName = api.replace('.yaml', '').replace(/^\w/, c => c.toUpperCase());
if (serviceName.toLowerCase() === 'http') {
serviceName = 'HTTP';
}
option.textContent = serviceName;
if (api === currentService) {
option.selected = true;
}
dropdown.appendChild(option);
});
// Handle dropdown change
dropdown.addEventListener('change', (e) => {
const selectedApi = e.target.value;
loadSwaggerUI(selectedApi);
updateURL(selectedApi);
});
}
// Load Swagger UI with specified API spec
function loadSwaggerUI(apiSpec) {
SwaggerUIBundle({
url: apiSpec,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
}
// Initialize
createServiceDropdown();
loadSwaggerUI(getServiceFromURL());
</script>
</body>
</html>
+75
View File
@@ -0,0 +1,75 @@
# Manager
Manager provides an HTTP API for managing platform resources: users, devices,
applications and channels. Through this API clients are able to do the following
actions:
- register new accounts and obtain access tokens
- 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_DB_HOST | Database host address | localhost |
| MF_DB_PORT | Database host port | 5432 |
| MF_DB_USER | Database user | mainflux |
| MF_DB_PASSWORD | Database password | mainflux |
| MF_MANAGER_DB | Name of the database used by the service | manager |
| MF_MANAGER_PORT | Manager service HTTP port | 8180 |
| MF_MANAGER_SECRET | string used for signing tokens | manager |
## 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:
manager:
image: mainflux/manager:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_DB_HOST: [Database host address]
MF_DB_PORT: [Database host port]
MF_DB_USER: [Database user]
MF_DB_PASS: [Database password]
MF_MANAGER_DB: [Name of the database used by the service]
MF_MANAGER_PORT: [Service HTTP port]
MF_MANAGER_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/cmd/manager
# 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
# set the environment variables and run the service
MF_DB_HOST=[Database host address] MF_DB_PORT=[Database host port] MF_DB_USER=[Database user] MF_DB_PASS=[Database password] MF_MANAGER_DB=[Name of the database used by the service] MF_MANAGER_PORT=[Service HTTP port] MF_MANAGER_SECRET=[String used for signing tokens] app
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
+3
View File
@@ -0,0 +1,3 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
+281
View File
@@ -0,0 +1,281 @@
package api
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/manager"
)
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 {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(addClientReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.AddClient(req.key, req.client)
if err != nil {
return nil, err
}
return clientRes{id: id, created: true}, nil
}
}
func updateClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateClientReq)
if err := req.validate(); err != nil {
return nil, err
}
req.client.ID = req.id
if err := svc.UpdateClient(req.key, req.client); err != nil {
return nil, err
}
return clientRes{id: req.id, created: false}, nil
}
}
func viewClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, err
}
client, err := svc.ViewClient(req.key, req.id)
if err != nil {
return nil, err
}
return viewClientRes{client}, nil
}
}
func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listResourcesReq)
if err := req.validate(); err != nil {
return nil, err
}
clients, err := svc.ListClients(req.key)
if err != nil {
return nil, err
}
return listClientsRes{clients, len(clients)}, nil
}
}
func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
err := req.validate()
if err == manager.ErrNotFound {
return removeRes{}, nil
}
if err != nil {
return nil, err
}
if err = svc.RemoveClient(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(createChannelReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.CreateChannel(req.key, req.channel)
if err != nil {
return nil, err
}
return channelRes{id: id, created: true}, nil
}
}
func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateChannelReq)
if err := req.validate(); err != nil {
return nil, err
}
req.channel.ID = req.id
if err := svc.UpdateChannel(req.key, req.channel); err != nil {
return nil, err
}
return channelRes{id: req.id, created: false}, nil
}
}
func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, err
}
channel, err := svc.ViewChannel(req.key, req.id)
if err != nil {
return nil, err
}
return viewChannelRes{channel}, nil
}
}
func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listResourcesReq)
if err := req.validate(); err != nil {
return nil, err
}
channels, err := svc.ListChannels(req.key)
if err != nil {
return nil, err
}
return listChannelsRes{channels, len(channels)}, nil
}
}
func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
if err == manager.ErrNotFound {
return removeRes{}, nil
}
return nil, err
}
if err := svc.RemoveChannel(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
return nil, err
}
return connectionRes{}, nil
}
}
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
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
}
}
+242
View File
@@ -0,0 +1,242 @@
package api
import (
"time"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux/manager"
)
var _ manager.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc manager.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "register",
"email", user.Email,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Register(user)
}
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "login",
"email", user.Email,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Login(user)
}
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "add_client",
"key", key,
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.AddClient(key, client)
}
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "update_client",
"key", key,
"id", client.ID,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.UpdateClient(key, client)
}
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "view_client",
"key", key,
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.ViewClient(key, id)
}
func (lm *loggingMiddleware) ListClients(key string) (clients []manager.Client, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "list_clients",
"key", key,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.ListClients(key)
}
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "remove_client",
"key", key,
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.RemoveClient(key, id)
}
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "create_channel",
"key", key,
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.CreateChannel(key, channel)
}
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "update_channel",
"key", key,
"id", channel.ID,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.UpdateChannel(key, channel)
}
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "view_channel",
"key", key,
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.ViewChannel(key, id)
}
func (lm *loggingMiddleware) ListChannels(key string) (channels []manager.Channel, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "list_channels",
"key", key,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.ListChannels(key)
}
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "remove_channel",
"key", key,
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.RemoveChannel(key, id)
}
func (lm *loggingMiddleware) Connect(key, chanId, clientId string) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "connect",
"key", key,
"channel", chanId,
"client", clientId,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Connect(key, chanId, clientId)
}
func (lm *loggingMiddleware) Disconnect(key, chanId, clientId string) (err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "disconnect",
"key", key,
"channel", chanId,
"client", clientId,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Disconnect(key, chanId, clientId)
}
func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "identity",
"id", id,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.Identity(key)
}
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
defer func(begin time.Time) {
lm.logger.Log(
"method", "can_access",
"key", key,
"id", id,
"publisher", pub,
"error", err,
"took", time.Since(begin),
)
}(time.Now())
return lm.svc.CanAccess(key, id)
}
+170
View File
@@ -0,0 +1,170 @@
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/manager"
)
var _ manager.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc manager.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
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) {
defer func(begin time.Time) {
ms.counter.With("method", "add_client").Add(1)
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.AddClient(key, client)
}
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_client").Add(1)
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateClient(key, client)
}
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_client").Add(1)
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewClient(key, id)
}
func (ms *metricsMiddleware) ListClients(key string) ([]manager.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)
}
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_client").Add(1)
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveClient(key, id)
}
func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_channel").Add(1)
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CreateChannel(key, channel)
}
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_channel").Add(1)
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateChannel(key, channel)
}
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_channel").Add(1)
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewChannel(key, id)
}
func (ms *metricsMiddleware) ListChannels(key string) ([]manager.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)
}
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_channel").Add(1)
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveChannel(key, id)
}
func (ms *metricsMiddleware) Connect(key, chanId, clientId string) error {
defer func(begin time.Time) {
ms.counter.With("method", "connect").Add(1)
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Connect(key, chanId, clientId)
}
func (ms *metricsMiddleware) Disconnect(key, chanId, clientId string) error {
defer func(begin time.Time) {
ms.counter.With("method", "disconnect").Add(1)
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Disconnect(key, chanId, clientId)
}
func (ms *metricsMiddleware) 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)
}
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "can_access").Add(1)
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CanAccess(key, id)
}
+145
View File
@@ -0,0 +1,145 @@
package api
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/manager"
)
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 nil
}
type addClientReq struct {
key string
client manager.Client
}
func (req addClientReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
return req.client.Validate()
}
type updateClientReq struct {
key string
id string
client manager.Client
}
func (req updateClientReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return manager.ErrNotFound
}
return req.client.Validate()
}
type createChannelReq struct {
key string
channel manager.Channel
}
func (req createChannelReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
return nil
}
type updateChannelReq struct {
key string
id string
channel manager.Channel
}
func (req updateChannelReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return manager.ErrNotFound
}
return nil
}
type viewResourceReq struct {
key string
id string
}
func (req viewResourceReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return manager.ErrNotFound
}
return nil
}
type listResourcesReq struct {
key string
size int
offset int
}
func (req listResourcesReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if req.size > 0 && req.offset >= 0 {
return nil
}
return manager.ErrMalformedEntity
}
type connectionReq struct {
key string
chanId string
clientId string
}
func (req connectionReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.chanId) && !govalidator.IsUUID(req.clientId) {
return manager.ErrNotFound
}
return nil
}
+203
View File
@@ -0,0 +1,203 @@
package api
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
const wrong string = "?"
var (
client manager.Client = manager.Client{Type: "app"}
channel manager.Channel = manager.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},
}
for desc, tc := range cases {
req := identityReq{tc.key}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
client manager.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},
}
for desc, tc := range cases {
req := addClientReq{
key: tc.key,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
client manager.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},
}
for desc, tc := range cases {
req := updateClientReq{
key: tc.key,
id: tc.id,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
"valid channel creation request": {channel, key, nil},
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := createChannelReq{
key: tc.key,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
channel manager.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},
}
for desc, tc := range cases {
req := updateChannelReq{
key: tc.key,
id: tc.id,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewResourceReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
id string
key string
err error
}{
"valid resource viewing request": {id, key, nil},
"missing token": {id, "", manager.ErrUnauthorizedAccess},
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
}
for desc, tc := range cases {
req := viewResourceReq{tc.key, tc.id}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListResourcesReqValidation(t *testing.T) {
key := uuid.NewV4().String()
value := 10
cases := map[string]struct {
key string
size int
offset 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},
}
for desc, tc := range cases {
req := listResourcesReq{
key: tc.key,
size: tc.size,
offset: tc.offset,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
+216
View File
@@ -0,0 +1,216 @@
package api
import (
"fmt"
"net/http"
"github.com/mainflux/mainflux/manager"
)
const contentType = "application/json; charset=utf-8"
type apiRes interface {
code() int
headers() map[string]string
empty() bool
}
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 tokenRes struct {
Token string `json:"token,omitempty"`
}
func (res tokenRes) code() int {
return http.StatusCreated
}
func (res tokenRes) headers() map[string]string {
return map[string]string{}
}
func (res tokenRes) empty() bool {
return res.Token == ""
}
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 {
manager.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 []manager.Client `json:"clients"`
count int
}
func (res listClientsRes) code() int {
return http.StatusOK
}
func (res listClientsRes) headers() map[string]string {
return map[string]string{
"X-Count": fmt.Sprintf("%d", res.count),
}
}
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 {
manager.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 []manager.Channel `json:"channels"`
count int
}
func (res listChannelsRes) code() int {
return http.StatusOK
}
func (res listChannelsRes) headers() map[string]string {
return map[string]string{
"X-Count": fmt.Sprintf("%d", res.count),
}
}
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
}
+283
View File
@@ -0,0 +1,283 @@
package api
import (
"context"
"encoding/json"
"net/http"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/manager"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc manager.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,
encodeResponse,
opts...,
))
r.Put("/clients/:id", kithttp.NewServer(
updateClientEndpoint(svc),
decodeClientUpdate,
encodeResponse,
opts...,
))
r.Delete("/clients/:id", kithttp.NewServer(
removeClientEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/clients/:id", kithttp.NewServer(
viewClientEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/clients", kithttp.NewServer(
listClientsEndpoint(svc),
decodeList,
encodeResponse,
opts...,
))
r.Post("/channels", kithttp.NewServer(
createChannelEndpoint(svc),
decodeChannelCreation,
encodeResponse,
opts...,
))
r.Put("/channels/:id", kithttp.NewServer(
updateChannelEndpoint(svc),
decodeChannelUpdate,
encodeResponse,
opts...,
))
r.Delete("/channels/:id", kithttp.NewServer(
removeChannelEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/channels/:id", kithttp.NewServer(
viewChannelEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/channels", kithttp.NewServer(
listChannelsEndpoint(svc),
decodeList,
encodeResponse,
opts...,
))
r.Put("/channels/:chanId/clients/:clientId", kithttp.NewServer(
connectEndpoint(svc),
decodeConnection,
encodeResponse,
opts...,
))
r.Delete("/channels/:chanId/clients/:clientId", kithttp.NewServer(
disconnectEndpoint(svc),
decodeConnection,
encodeResponse,
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.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 err := json.NewDecoder(r.Body).Decode(&client); err != nil {
return nil, err
}
req := addClientReq{
key: r.Header.Get("Authorization"),
client: client,
}
return req, nil
}
func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error) {
var client manager.Client
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
return nil, err
}
req := updateClientReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
client: client,
}
return req, nil
}
func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, error) {
var channel manager.Channel
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
return nil, err
}
req := createChannelReq{
key: r.Header.Get("Authorization"),
channel: channel,
}
return req, nil
}
func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error) {
var channel manager.Channel
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
return nil, err
}
req := updateChannelReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
channel: channel,
}
return req, nil
}
func decodeView(_ context.Context, r *http.Request) (interface{}, error) {
req := viewResourceReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
}
return req, nil
}
func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
req := listResourcesReq{
key: r.Header.Get("Authorization"),
size: 10,
offset: 0,
}
return req, nil
}
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"),
}
return req, nil
}
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() {
w.Header().Set(k, v)
}
w.WriteHeader(ar.code())
if ar.empty() {
return nil
}
}
return json.NewEncoder(w).Encode(response)
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", contentType)
switch err {
case manager.ErrMalformedEntity:
w.WriteHeader(http.StatusBadRequest)
case manager.ErrUnauthorizedAccess:
w.WriteHeader(http.StatusForbidden)
case manager.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
case manager.ErrConflict:
w.WriteHeader(http.StatusConflict)
default:
if _, ok := err.(*json.SyntaxError); ok {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusInternalServerError)
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package bcrypt provides a hasher implementation utilising bcrypt.
package bcrypt
+30
View File
@@ -0,0 +1,30 @@
package bcrypt
import (
"github.com/mainflux/mainflux/manager"
"golang.org/x/crypto/bcrypt"
)
const cost int = 10
var _ manager.Hasher = (*bcryptHasher)(nil)
type bcryptHasher struct{}
// New instantiates a bcrypt-based hasher implementation.
func New() manager.Hasher {
return &bcryptHasher{}
}
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
if err != nil {
return "", err
}
return string(hash), nil
}
func (bh *bcryptHasher) Compare(plain, hashed string) error {
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
}
+44
View File
@@ -0,0 +1,44 @@
package manager
// 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:"-"`
Name string `json:"name,omitempty"`
Clients []Client `gorm:"many2many:channel_clients" json:"connected,omitempty"`
}
// ChannelRepository specifies a channel persistence API.
type ChannelRepository interface {
// Save persists the channel. Successful operation is indicated by unique
// identifier accompanied by nil error response. A non-nil error is
// returned to indicate operation failure.
Save(Channel) (string, error)
// Update performs an update to the existing channel. A non-nil error is
// returned to indicate operation failure.
Update(Channel) error
// One retrieves the channel having the provided identifier, that is owned
// by the specified user.
One(string, string) (Channel, error)
// All retrieves the channels owned by the specified user.
All(string) []Channel
// Remove removes the channel having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// HasClient determines whether the client with the provided identifier, is
// "connected" to the specified channel.
HasClient(string, string) bool
}
+104
View File
@@ -0,0 +1,104 @@
// Package client provides a manager service client intended for internal
// service communication.
package client
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/mainflux/mainflux/manager"
"github.com/sony/gobreaker"
)
const (
timeout = time.Second * 5
maxFailedReqs = 3
maxFailureRatio = 0.6
)
var (
// ErrServiceUnreachable indicates that the service instance is not available.
ErrServiceUnreachable = errors.New("manager service unavailable")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = manager.ErrUnauthorizedAccess
)
// ManagerClient provides an access to the manager service authorization
// endpoints.
type ManagerClient struct {
url string
cb *gobreaker.CircuitBreaker
}
// NewClient instantiates the manager service client given its base URL.
func NewClient(url string) ManagerClient {
st := gobreaker.Settings{
Name: "Manager",
ReadyToTrip: func(counts gobreaker.Counts) bool {
fr := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= maxFailedReqs && fr >= maxFailureRatio
},
}
mc := ManagerClient{
url: url,
cb: gobreaker.NewCircuitBreaker(st),
}
return mc
}
// VerifyToken tries to extract an identity from the provided token.
func (mc ManagerClient) VerifyToken(token string) (string, error) {
url := fmt.Sprintf("%s/access-grant", mc.url)
return mc.makeRequest(url, token)
}
// CanAccess checks whether or not the client having a provided token has
// access to the specified channel.
func (mc ManagerClient) CanAccess(channel, token string) (string, error) {
url := fmt.Sprintf("%s/channels/%s/access-grant", mc.url, channel)
return mc.makeRequest(url, token)
}
func (mc ManagerClient) makeRequest(url, token string) (string, error) {
response, err := mc.cb.Execute(func() (interface{}, error) {
hc := &http.Client{
Timeout: timeout,
}
mgReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", ErrServiceUnreachable
}
mgReq.Header.Set("Authorization", token)
res, err := hc.Do(mgReq)
if err != nil {
return "", ErrServiceUnreachable
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ErrUnauthorizedAccess, nil
}
return res.Header.Get("X-client-id"), nil
})
if err != nil {
return "", err
}
id, ok := response.(string)
if !ok {
return "", manager.ErrUnauthorizedAccess
}
return id, nil
}
+53
View File
@@ -0,0 +1,53 @@
package manager
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"`
Name string `json:"name,omitempty"`
Key string `json:"key"`
Payload string `json:"payload,omitempty"`
}
var clientTypes map[string]bool = map[string]bool{
"app": true,
"device": true,
}
// Validate returns an error if client representation is invalid.
func (c *Client) Validate() error {
if c.Type = strings.ToLower(c.Type); !clientTypes[c.Type] {
return ErrMalformedEntity
}
return nil
}
// ClientRepository specifies a client persistence API.
type ClientRepository interface {
// Id generates new resource identifier.
Id() string
// Save persists the client. Successful operation is indicated by non-nil
// error response.
Save(Client) error
// Update performs an update to the existing client. A non-nil error is
// returned to indicate operation failure.
Update(Client) error
// One retrieves the client having the provided identifier, that is owned
// by the specified user.
One(string, string) (Client, error)
// All retrieves the clients owned by the specified user.
All(string) []Client
// Remove removes the client having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
}
+3
View File
@@ -0,0 +1,3 @@
// Package manager contains the domain concept definitions needed to support
// Mainflux manager service functionality.
package manager
+2
View File
@@ -0,0 +1,2 @@
// Package jwt provides a JWT identity provider.
package jwt
+73
View File
@@ -0,0 +1,73 @@
package jwt
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/mainflux/mainflux/manager"
)
const (
issuer string = "mainflux"
duration time.Duration = 10 * time.Hour
)
var _ manager.IdentityProvider = (*jwtIdentityProvider)(nil)
type jwtIdentityProvider struct {
secret string
}
// New instantiates a JWT identity provider.
func New(secret string) manager.IdentityProvider {
return &jwtIdentityProvider{}
}
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
now := time.Now().UTC()
exp := now.Add(duration)
claims := jwt.StandardClaims{
Subject: id,
Issuer: issuer,
IssuedAt: now.Unix(),
ExpiresAt: exp.Unix(),
}
return idp.jwt(claims)
}
func (idp *jwtIdentityProvider) PermanentKey(id string) (string, error) {
claims := jwt.StandardClaims{
Subject: id,
Issuer: issuer,
IssuedAt: time.Now().UTC().Unix(),
}
return idp.jwt(claims)
}
func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(idp.secret))
}
func (idp *jwtIdentityProvider) Identity(key string) (string, error) {
token, err := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, manager.ErrUnauthorizedAccess
}
return []byte(idp.secret), nil
})
if err != nil {
return "", manager.ErrUnauthorizedAccess
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims["sub"].(string), nil
}
return "", manager.ErrUnauthorizedAccess
}
+231
View File
@@ -0,0 +1,231 @@
package manager
var _ Service = (*managerService)(nil)
type managerService struct {
users UserRepository
clients ClientRepository
channels ChannelRepository
hasher Hasher
idp IdentityProvider
}
// New instantiates the domain service implementation.
func New(users UserRepository, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
return &managerService{
users: users,
clients: clients,
channels: channels,
hasher: hasher,
idp: idp,
}
}
func (ms *managerService) Register(user User) error {
hash, err := ms.hasher.Hash(user.Password)
if err != nil {
return ErrMalformedEntity
}
user.Password = hash
return ms.users.Save(user)
}
func (ms *managerService) Login(user User) (string, error) {
dbUser, err := ms.users.One(user.Email)
if err != nil {
return "", ErrUnauthorizedAccess
}
if err := ms.hasher.Compare(user.Password, dbUser.Password); err != nil {
return "", ErrUnauthorizedAccess
}
return ms.idp.TemporaryKey(user.Email)
}
func (ms *managerService) AddClient(key string, client Client) (string, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if _, err := ms.users.One(sub); err != nil {
return "", ErrUnauthorizedAccess
}
client.ID = ms.clients.Id()
client.Owner = sub
client.Key, _ = ms.idp.PermanentKey(client.ID)
return client.ID, ms.clients.Save(client)
}
func (ms *managerService) UpdateClient(key string, client Client) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
client.Owner = sub
return ms.clients.Update(client)
}
func (ms *managerService) ViewClient(key, id string) (Client, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return Client{}, err
}
if _, err := ms.users.One(sub); err != nil {
return Client{}, ErrUnauthorizedAccess
}
return ms.clients.One(sub, id)
}
func (ms *managerService) ListClients(key string) ([]Client, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return nil, err
}
if _, err := ms.users.One(sub); err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.clients.All(sub), nil
}
func (ms *managerService) RemoveClient(key, id string) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
return ms.clients.Remove(sub, id)
}
func (ms *managerService) CreateChannel(key string, channel Channel) (string, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if _, err := ms.users.One(sub); err != nil {
return "", ErrUnauthorizedAccess
}
channel.Owner = sub
return ms.channels.Save(channel)
}
func (ms *managerService) UpdateChannel(key string, channel Channel) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
channel.Owner = sub
return ms.channels.Update(channel)
}
func (ms *managerService) ViewChannel(key, id string) (Channel, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return Channel{}, err
}
if _, err := ms.users.One(sub); err != nil {
return Channel{}, ErrUnauthorizedAccess
}
return ms.channels.One(sub, id)
}
func (ms *managerService) ListChannels(key string) ([]Channel, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return nil, err
}
if _, err := ms.users.One(sub); err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.channels.All(sub), nil
}
func (ms *managerService) RemoveChannel(key, id string) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Remove(sub, id)
}
func (ms *managerService) Connect(key, chanId, clientId string) error {
owner, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(owner); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Connect(owner, chanId, clientId)
}
func (ms *managerService) Disconnect(key, chanId, clientId string) error {
owner, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(owner); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Disconnect(owner, chanId, clientId)
}
func (ms *managerService) Identity(key string) (string, error) {
client, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
return client, nil
}
func (ms *managerService) 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
}
+375
View File
@@ -0,0 +1,375 @@
package manager_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/mocks"
"github.com/stretchr/testify/assert"
)
const wrong string = "wrong-value"
var (
user manager.User = manager.User{"user@example.com", "password"}
client manager.Client = manager.Client{ID: "1", Type: "app", Name: "test", Key: "1"}
channel manager.Channel = manager.Channel{ID: "1", Name: "test", Clients: []manager.Client{client}}
)
func newService() manager.Service {
users := mocks.NewUserRepository()
clients := mocks.NewClientRepository()
channels := mocks.NewChannelRepository()
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return manager.New(users, clients, channels, hasher, idp)
}
func TestRegister(t *testing.T) {
svc := newService()
cases := map[string]struct {
user manager.User
err error
}{
"register new user": {user, nil},
"register existing user": {user, manager.ErrConflict},
}
for desc, tc := range cases {
err := svc.Register(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestLogin(t *testing.T) {
svc := newService()
svc.Register(user)
cases := map[string]struct {
user manager.User
err error
}{
"login with good credentials": {user, nil},
"login with wrong e-mail": {manager.User{wrong, user.Password}, manager.ErrUnauthorizedAccess},
"login with wrong password": {manager.User{user.Email, wrong}, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.Login(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClient(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
client manager.Client
key string
err error
}{
"add new app": {manager.Client{Type: "app", Name: "a"}, key, nil},
"add new device": {manager.Client{Type: "device", Name: "b"}, key, nil},
"add client with wrong credentials": {manager.Client{Type: "app", Name: "d"}, wrong, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
cases := map[string]struct {
client manager.Client
key string
err error
}{
"update existing client": {client, key, nil},
"update client with wrong credentials": {client, wrong, manager.ErrUnauthorizedAccess},
"update non-existing client": {manager.Client{ID: "2", Type: "app", Name: "d"}, key, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
cases := map[string]struct {
id string
key string
err error
}{
"view existing client": {client.ID, key, nil},
"view client with wrong credentials": {client.ID, wrong, manager.ErrUnauthorizedAccess},
"view non-existing client": {wrong, key, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
"list clients": {key, nil},
"list clients with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.ListClients(tc.key)
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()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
cases := map[string]struct {
id string
key string
err error
}{
"remove client with wrong credentials": {client.ID, "?", manager.ErrUnauthorizedAccess},
"remove existing client": {client.ID, key, nil},
"remove removed client": {client.ID, key, nil},
"remove non-existing client": {"?", key, 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()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
"create channel": {manager.Channel{}, key, nil},
"create channel with wrong credentials": {manager.Channel{}, wrong, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
svc.CreateChannel(key, channel)
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
"update existing channel": {channel, key, nil},
"update channel with wrong credentials": {channel, wrong, manager.ErrUnauthorizedAccess},
"update non-existing channel": {manager.Channel{ID: "2", Name: "test"}, key, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
svc.CreateChannel(key, channel)
cases := map[string]struct {
id string
key string
err error
}{
"view existing channel": {channel.ID, key, nil},
"view channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
"view non-existing channel": {wrong, key, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
"list channels": {key, nil},
"list channels with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.ListChannels(tc.key)
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()
svc.Register(user)
key, _ := svc.Login(user)
svc.CreateChannel(key, channel)
cases := map[string]struct {
id string
key string
err error
}{
"remove channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
"remove existing channel": {channel.ID, key, nil},
"remove removed channel": {channel.ID, key, nil},
"remove non-existing channel": {channel.ID, key, 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()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
chanId, _ := svc.CreateChannel(key, channel)
cases := map[string]struct {
key string
chanId string
clientId string
err error
}{
"connect client": {key, chanId, clientId, nil},
"connect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
"connect client to non-existing channel": {key, wrong, clientId, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
chanId, _ := svc.CreateChannel(key, channel)
svc.Connect(key, chanId, clientId)
cases := map[string]struct {
key string
chanId string
clientId string
err error
}{
"disconnect connected client": {key, chanId, clientId, nil},
"disconnect disconnected client": {key, chanId, clientId, manager.ErrNotFound},
"disconnect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
"disconnect client from non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
"disconnect non-existing client": {key, chanId, wrong, manager.ErrNotFound},
}
for desc, 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", desc, tc.err, err))
}
}
func TestIdentity(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
"valid token's identity": {key, nil},
"invalid token's identity": {"", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.Identity(tc.key)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCanAccess(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
svc.AddClient(key, client)
svc.CreateChannel(key, channel)
cases := map[string]struct {
key string
channel string
err error
}{
"allowed access": {client.Key, channel.ID, nil},
"not-connected cannot access": {wrong, channel.ID, manager.ErrUnauthorizedAccess},
"access non-existing channel": {client.Key, wrong, manager.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))
}
}
+133
View File
@@ -0,0 +1,133 @@
package mocks
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/mainflux/mainflux/manager"
)
var _ manager.ChannelRepository = (*channelRepositoryMock)(nil)
type channelRepositoryMock struct {
mu sync.Mutex
counter int
channels map[string]manager.Channel
}
// NewChannelRepository creates in-memory channel repository.
func NewChannelRepository() manager.ChannelRepository {
return &channelRepositoryMock{
channels: make(map[string]manager.Channel),
}
}
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter += 1
channel.ID = strconv.Itoa(crm.counter)
crm.channels[key(channel.Owner, channel.ID)] = channel
return channel.ID, nil
}
func (crm *channelRepositoryMock) Update(channel manager.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(channel.Owner, channel.ID)
if _, ok := crm.channels[dbKey]; !ok {
return manager.ErrNotFound
}
crm.channels[dbKey] = channel
return nil
}
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
if c, ok := crm.channels[key(owner, id)]; ok {
return c, nil
}
return manager.Channel{}, manager.ErrNotFound
}
func (crm *channelRepositoryMock) All(owner string) []manager.Channel {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
channels := make([]manager.Channel, 0)
for k, v := range crm.channels {
if strings.HasPrefix(k, prefix) {
channels = append(channels, v)
}
}
return channels
}
func (crm *channelRepositoryMock) Remove(owner, id string) error {
delete(crm.channels, key(owner, id))
return nil
}
func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error {
channel, err := crm.One(owner, chanId)
if err != nil {
return err
}
// 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})
return crm.Update(channel)
}
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
channel, err := crm.One(owner, chanId)
if err != nil {
return err
}
if !crm.HasClient(chanId, clientId) {
return manager.ErrNotFound
}
connected := make([]manager.Client, len(channel.Clients)-1)
for _, client := range channel.Clients {
if client.ID != clientId {
connected = append(connected, client)
}
}
channel.Clients = connected
return crm.Update(channel)
}
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
suffix := fmt.Sprintf("-%s", channel)
for k, v := range crm.channels {
if strings.HasSuffix(k, suffix) {
for _, c := range v.Clients {
if c.ID == client {
return true
}
}
break
}
}
return false
}
+86
View File
@@ -0,0 +1,86 @@
package mocks
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/mainflux/mainflux/manager"
)
var _ manager.ClientRepository = (*clientRepositoryMock)(nil)
type clientRepositoryMock struct {
mu sync.Mutex
counter int
clients map[string]manager.Client
}
// NewClientRepository creates in-memory client repository.
func NewClientRepository() manager.ClientRepository {
return &clientRepositoryMock{
clients: make(map[string]manager.Client),
}
}
func (crm *clientRepositoryMock) Id() string {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter += 1
return strconv.Itoa(crm.counter)
}
func (crm *clientRepositoryMock) Save(client manager.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.clients[key(client.Owner, client.ID)] = client
return nil
}
func (crm *clientRepositoryMock) Update(client manager.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(client.Owner, client.ID)
if _, ok := crm.clients[dbKey]; !ok {
return manager.ErrNotFound
}
crm.clients[dbKey] = client
return nil
}
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
if c, ok := crm.clients[key(owner, id)]; ok {
return c, nil
}
return manager.Client{}, manager.ErrNotFound
}
func (crm *clientRepositoryMock) All(owner string) []manager.Client {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
clients := make([]manager.Client, 0)
for k, v := range crm.clients {
if strings.HasPrefix(k, prefix) {
clients = append(clients, v)
}
}
return clients
}
func (crm *clientRepositoryMock) Remove(owner, id string) error {
delete(crm.clients, key(owner, id))
return nil
}
+11
View File
@@ -0,0 +1,11 @@
package mocks
import "fmt"
// Since mocks will store data in map, and they need to resemble the real
// identifiers as much as possible, a key will be created as combination of
// owner and their own identifiers. This will allow searching either by
// prefix or suffix.
func key(owner, id string) string {
return fmt.Sprintf("%s-%s", owner, id)
}
+52
View File
@@ -0,0 +1,52 @@
package mocks
import "github.com/mainflux/mainflux/manager"
var (
_ manager.Hasher = (*hasherMock)(nil)
_ manager.IdentityProvider = (*identityProviderMock)(nil)
)
type hasherMock struct{}
func (hm *hasherMock) Hash(pwd string) (string, error) {
return pwd, nil
}
func (hm *hasherMock) Compare(plain, hashed string) error {
if plain != hashed {
return manager.ErrUnauthorizedAccess
}
return nil
}
type identityProviderMock struct{}
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
if id == "" {
return "", manager.ErrUnauthorizedAccess
}
return id, nil
}
func (idp *identityProviderMock) PermanentKey(id string) (string, error) {
return idp.TemporaryKey(id)
}
func (idp *identityProviderMock) Identity(key string) (string, error) {
return idp.TemporaryKey(key)
}
// NewHasher creates "no-op" hasher for test purposes. This implementation will
// return secrets without changing them.
func NewHasher() manager.Hasher {
return &hasherMock{}
}
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
// token will hold value provided by the caller.
func NewIdentityProvider() manager.IdentityProvider {
return &identityProviderMock{}
}
+44
View File
@@ -0,0 +1,44 @@
package mocks
import (
"sync"
"github.com/mainflux/mainflux/manager"
)
var _ manager.UserRepository = (*userRepositoryMock)(nil)
type userRepositoryMock struct {
mu sync.Mutex
users map[string]manager.User
}
// NewUserRepository creates in-memory user repository.
func NewUserRepository() manager.UserRepository {
return &userRepositoryMock{
users: make(map[string]manager.User),
}
}
func (urm *userRepositoryMock) Save(user manager.User) error {
urm.mu.Lock()
defer urm.mu.Unlock()
if _, ok := urm.users[user.Email]; ok {
return manager.ErrConflict
}
urm.users[user.Email] = user
return nil
}
func (urm *userRepositoryMock) One(email string) (manager.User, error) {
urm.mu.Lock()
defer urm.mu.Unlock()
if val, ok := urm.users[email]; ok {
return val, nil
}
return manager.User{}, manager.ErrUnauthorizedAccess
}
+119
View File
@@ -0,0 +1,119 @@
package postgres
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
)
var _ manager.ChannelRepository = (*channelRepository)(nil)
type channelRepository struct {
db *gorm.DB
}
func NewChannelRepository(db *gorm.DB) manager.ChannelRepository {
return &channelRepository{db}
}
func (cr channelRepository) Save(channel manager.Channel) (string, error) {
channel.ID = uuid.NewV4().String()
if err := cr.db.Create(&channel).Error; err != nil {
return "", err
}
return channel.ID, nil
}
func (cr channelRepository) Update(channel manager.Channel) error {
sql := "UPDATE channels SET name = ? WHERE owner = ? AND id = ?;"
res := cr.db.Exec(sql, channel.Name, channel.Owner, channel.ID)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
channel := manager.Channel{}
res := cr.db.Preload("Clients").First(&channel, "owner = ? AND id = ?", owner, id)
if err := res.Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return channel, manager.ErrNotFound
}
return channel, err
}
return channel, nil
}
func (cr channelRepository) All(owner string) []manager.Channel {
var channels []manager.Channel
cr.db.Find(&channels, "owner = ?", owner)
return channels
}
func (cr channelRepository) Remove(owner, id string) error {
cr.db.Delete(&manager.Channel{}, "owner = ? AND id = ?", owner, id)
return nil
}
func (cr channelRepository) Connect(owner, chanId, clientId string) error {
// This approach can be replaced by declaring composite keys on both tables
// (clients and channels), and then propagate them into the m2m table. For
// some reason GORM does not infer these kind of connections well and
// raises a "no unique constraint for referenced table". Until we find a
// way to properly represent this relationship, let's stick with the nested
// query approach and observe its behaviour.
sql := `INSERT INTO channel_clients (channel_id, client_id)
SELECT ?, ? WHERE
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr channelRepository) Disconnect(owner, chanId, clientId string) error {
// The same remark given in Connect applies here.
sql := `DELETE FROM channel_clients WHERE
channel_id = ? AND client_id = ? AND
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr channelRepository) HasClient(chanId, clientId string) bool {
sql := "SELECT EXISTS (SELECT 1 FROM channel_clients WHERE channel_id = $1 AND client_id = $2);"
row := cr.db.DB().QueryRow(sql, chanId, clientId)
var exists bool
if err := row.Scan(&exists); err != nil {
// TODO: this error should be logged
return false
}
return exists
}
+246
View File
@@ -0,0 +1,246 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/postgres"
"github.com/stretchr/testify/assert"
)
func TestChannelSave(t *testing.T) {
email := "channel-save@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
c1 := manager.Channel{Owner: email}
c2 := manager.Channel{Owner: wrong}
cases := map[string]struct {
channel manager.Channel
hasErr bool
}{
"new channel, existing user": {c1, false},
"new channel, non-existing user": {c2, true},
}
channelRepo := postgres.NewChannelRepository(db)
for desc, tc := range cases {
_, err := channelRepo.Save(tc.channel)
hasErr := err != nil
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t", desc, tc.hasErr, hasErr))
}
}
func TestChannelUpdate(t *testing.T) {
email := "channel-update@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
c := manager.Channel{Owner: email}
id, _ := chanRepo.Save(c)
c.ID = id
cases := map[string]struct {
channel manager.Channel
err error
}{
"existing channel": {c, nil},
"non-existing channel with existing user": {manager.Channel{ID: wrong, Owner: email}, manager.ErrNotFound},
"non-existing channel with non-existing user": {manager.Channel{ID: wrong, Owner: wrong}, manager.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"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
c := manager.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, manager.ErrNotFound},
"non-existing owner": {wrong, id, manager.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"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
n := 10
for i := 0; i < n; i++ {
c := manager.Channel{Owner: email}
chanRepo.Save(c)
}
cases := map[string]struct {
owner string
len int
}{
"existing owner": {email, n},
"non-existing owner": {wrong, 0},
}
for desc, tc := range cases {
n := len(chanRepo.All(tc.owner))
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
}
}
func TestChannelRemoval(t *testing.T) {
email := "channel-removal@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.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 != manager.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
}
}
}
func TestChannelConnect(t *testing.T) {
email := "channel-connect@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
cases := map[string]struct {
owner string
chanId string
clientId string
err error
}{
"existing user, channel and client": {email, chanId, client.ID, nil},
"with non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
}
for desc, 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", desc, tc.err, err))
}
}
func TestChannelDisconnect(t *testing.T) {
email := "channel-disconnect@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
chanRepo.Connect(email, chanId, client.ID)
cases := map[string]struct {
owner string
chanId string
clientId string
err error
}{
"connected client": {email, chanId, client.ID, nil},
"non-connected client": {email, chanId, client.ID, manager.ErrNotFound},
"non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
}
for desc, 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", desc, tc.err, err))
}
}
func TestChannelAccessCheck(t *testing.T) {
email := "channel-access-check@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db)
chanId, _ := chanRepo.Save(manager.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))
}
}
+72
View File
@@ -0,0 +1,72 @@
package postgres
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
)
var _ manager.ClientRepository = (*clientRepository)(nil)
type clientRepository struct {
db *gorm.DB
}
// NewClientRepository instantiates a PostgreSQL implementation of client
// repository.
func NewClientRepository(db *gorm.DB) manager.ClientRepository {
return &clientRepository{db}
}
func (cr *clientRepository) Id() string {
return uuid.NewV4().String()
}
func (cr *clientRepository) Save(client manager.Client) error {
if err := cr.db.Create(&client).Error; err != nil {
return err
}
return nil
}
func (cr *clientRepository) Update(client manager.Client) error {
sql := "UPDATE clients SET name = ?, payload = ? WHERE owner = ? AND id = ?;"
res := cr.db.Exec(sql, client.Name, client.Payload, client.Owner, client.ID)
if res.Error == nil && res.RowsAffected == 0 {
return manager.ErrNotFound
}
return res.Error
}
func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
client := manager.Client{}
res := cr.db.First(&client, "owner = ? AND id = ?", owner, id)
if err := res.Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return client, manager.ErrNotFound
}
return client, err
}
return client, nil
}
func (cr *clientRepository) All(owner string) []manager.Client {
var clients []manager.Client
cr.db.Find(&clients, "owner = ?", owner)
return clients
}
func (cr *clientRepository) Remove(owner, id string) error {
cr.db.Delete(&manager.Client{}, "owner = ? AND id = ?", owner, id)
return nil
}
+162
View File
@@ -0,0 +1,162 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/postgres"
"github.com/stretchr/testify/assert"
)
func TestClientSave(t *testing.T) {
email := "client-save@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
c1 := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
c2 := manager.Client{
ID: clientRepo.Id(),
Owner: "unknown@example.com",
}
cases := map[string]struct {
client manager.Client
hasErr bool
}{
"new client, existing user": {c1, false},
"new client, non-existing user": {c2, true},
}
for desc, tc := range cases {
hasErr := clientRepo.Save(tc.client) != nil
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasErr, hasErr))
}
}
func TestClientUpdate(t *testing.T) {
email := "client-update@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
c := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
client manager.Client
err error
}{
"existing client": {c, nil},
"non-existing client with existing user": {manager.Client{ID: wrong, Owner: email}, manager.ErrNotFound},
"non-existing client with non-existing user": {manager.Client{ID: wrong, Owner: wrong}, manager.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"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
c := manager.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, manager.ErrNotFound},
"non-existing owner": {wrong, c.ID, manager.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"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
n := 10
for i := 0; i < n; i++ {
c := manager.Client{
ID: clientRepo.Id(),
Owner: email,
}
clientRepo.Save(c)
}
cases := map[string]struct {
owner string
len int
}{
"existing owner": {email, n},
"non-existing owner": {wrong, 0},
}
for desc, tc := range cases {
n := len(clientRepo.All(tc.owner))
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
}
}
func TestClientRemoval(t *testing.T) {
email := "client-removal@example.com"
userRepo := postgres.NewUserRepository(db)
userRepo.Save(manager.User{email, "pass"})
clientRepo := postgres.NewClientRepository(db)
client := manager.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 != manager.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
// Package postgres contains repository implementations using PostgreSQL as
// the underlying database.
package postgres
+48
View File
@@ -0,0 +1,48 @@
package postgres
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/mainflux/mainflux/manager"
)
const errDuplicate string = "unique_violation"
type connection struct {
ClientID string `gorm:"primary_key"`
ChannelID string `gorm:"primary_key"`
}
func (c connection) TableName() string {
return "channel_clients"
}
// Connect creates a connection to the PostgreSQL instance. A non-nil error
// is returned to indicate failure.
func Connect(host, port, name, user, pass string) (*gorm.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 := gorm.Open("postgres", url)
if err != nil {
return nil, err
}
db = db.AutoMigrate(&manager.User{}, &manager.Client{}, &manager.Channel{}, &connection{})
db = db.Model(&manager.Client{}).
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
AddUniqueIndex("idx_pk_client", "id", "owner")
db = db.Model(&manager.Channel{}).
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
AddUniqueIndex("idx_pk_channel", "id", "owner")
db = db.Model(&connection{}).
AddForeignKey("client_id", "clients(id)", "CASCADE", "CASCADE").
AddForeignKey("channel_id", "channels(id)", "CASCADE", "CASCADE")
return db.LogMode(false), nil
}
+62
View File
@@ -0,0 +1,62 @@
// Package postgres_test contains tests for PostgreSQL repository
// implementations.
package postgres_test
import (
"database/sql"
"fmt"
"log"
"os"
"testing"
"github.com/jinzhu/gorm"
"github.com/mainflux/mainflux/manager/postgres"
"gopkg.in/ory-am/dockertest.v3"
)
const wrong string = "wrong-value"
var db *gorm.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)
}
+48
View File
@@ -0,0 +1,48 @@
package postgres
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/lib/pq"
"github.com/mainflux/mainflux/manager"
)
var _ manager.UserRepository = (*userRepository)(nil)
type userRepository struct {
db *gorm.DB
}
// NewUserRepository instantiates a PostgreSQL implementation of user
// repository.
func NewUserRepository(db *gorm.DB) manager.UserRepository {
return &userRepository{db}
}
func (ur *userRepository) Save(user manager.User) error {
if err := ur.db.Create(&user).Error; err != nil {
if pqErr, ok := err.(*pq.Error); ok && errDuplicate == pqErr.Code.Name() {
return manager.ErrConflict
}
return err
}
return nil
}
func (ur *userRepository) One(email string) (manager.User, error) {
user := manager.User{}
q := ur.db.First(&user, "email = ?", email)
if err := q.Error; err != nil {
if err == gorm.ErrRecordNotFound {
return user, manager.ErrNotFound
}
return user, err
}
return user, nil
}
+49
View File
@@ -0,0 +1,49 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/postgres"
"github.com/stretchr/testify/assert"
)
func TestUserSave(t *testing.T) {
email := "user-save@example.com"
cases := map[string]struct {
user manager.User
err error
}{
"new user": {manager.User{email, "pass"}, nil},
"duplicate user": {manager.User{email, "pass"}, manager.ErrConflict},
}
repo := postgres.NewUserRepository(db)
for desc, tc := range cases {
err := repo.Save(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleUserRetrieval(t *testing.T) {
email := "user-retrieval@example.com"
repo := postgres.NewUserRepository(db)
repo.Save(manager.User{email, "pass"})
cases := map[string]struct {
email string
err error
}{
"existing user": {email, nil},
"non-existing user": {"unknown@example.com", manager.ErrNotFound},
}
for desc, tc := range cases {
_, err := repo.One(tc.email)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
+25
View File
@@ -0,0 +1,25 @@
package manager
// Hasher specifies an API for generating hashes of an arbitrary textual
// content.
type Hasher interface {
// Hash generates the hashed string from plain-text.
Hash(string) (string, error)
// Compare compares plain-text version to the hashed one. An error should
// indicate failed comparison.
Compare(string, string) error
}
// IdentityProvider specifies an API for identity management via security
// tokens.
type IdentityProvider interface {
// TemporaryKey generates the temporary access token.
TemporaryKey(string) (string, error)
// PermanentKey generates the non-expiring access token.
PermanentKey(string) (string, error)
// Identity extracts the entity identifier given its secret key.
Identity(string) (string, error)
}
+85
View File
@@ -0,0 +1,85 @@
package manager
import "errors"
var (
// ErrConflict indicates usage of the existing email during account
// registration.
ErrConflict error = errors.New("email already taken")
// ErrMalformedEntity indicates malformed entity specification (e.g.
// invalid username or password).
ErrMalformedEntity error = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess error = errors.New("missing or invalid credentials provided")
// ErrNotFound indicates a non-existent entity request.
ErrNotFound error = 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 {
// Register creates new user account. In case of the failed registration, a
// non-nil error value is returned.
Register(User) error
// Login authenticates the user given its credentials. Successful
// authentication generates new access token. Failed invocations are
// identified by the non-nil error values in the response.
Login(User) (string, error)
// 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 all clients that belongs to the user
// identified by the provided key.
ListClients(string) ([]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 all clients that belongs to the user
// identified by the provided key.
ListChannels(string) ([]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
// Identity retrieves Client ID for provided client token.
Identity(string) (string, error)
// CanAccess determines whether the channel can be accessed using the
// provided key.
CanAccess(string, string) (string, error)
}
+548
View File
@@ -0,0 +1,548 @@
swagger: "2.0"
info:
title: Mainflux manager service
description: HTTP API for managing platform users, 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
description: |
Adds new client to the list of clients owned by user identified using
the provided access token.
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- name: client
description: JSON-formatted document describing the new client.
in: body
schema:
$ref: "#/definitions/ClientReq"
required: true
responses:
201:
description: Client registered.
headers:
Location:
type: string
description: Created client's relative URL (i.e. /clients/{clientId}).
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
get:
summary: Retrieves managed clients
description: |
Retrieves a list of managed clients. Due to performance concerns, data
is retrieved in subsets. The API clients must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Size"
- $ref: "#/parameters/Offset"
responses:
200:
description: Data retrieved.
headers:
X-Count:
type: integer
description: |
Total number of managed clients. This value can be used to
implement the paging strategy on API clients.
schema:
$ref: "#/definitions/ClientList"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/clients/{clientId}:
get:
summary: Retrieves client info
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ClientId"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/ClientRes"
403:
description: Missing or invalid access token provided.
404:
description: Client does not exist.
500:
$ref: "#/responses/ServiceError"
put:
summary: Updates client info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the client's type and ID
cannot be changed.
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ClientId"
- name: client
description: JSON-formatted document describing the updated client.
in: body
schema:
$ref: "#/definitions/ClientReq"
required: true
responses:
200:
description: Client updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Client does not exist.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Removes a client
description: |
Removes a client. The service will ensure that the removed client is
disconnected from all of the existing channels.
tags:
- clients
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ClientId"
responses:
204:
description: Client removed.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/channels:
post:
summary: Creates new channel
description: |
Creates new channel. User identified by the provided access token will
be the channel's owner.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- name: channel
description: JSON-formatted document describing the new channel.
in: body
schema:
$ref: "#/definitions/ChannelReq"
required: true
responses:
201:
description: Channel created.
headers:
Location:
type: string
description: Created channel's relative URL (i.e. /channels/{chanId}).
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
get:
summary: Retrieves managed channels
description: |
Retrieves a list of managed channels. Due to performance concerns, data
is retrieved in subsets. The API clients must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Size"
- $ref: "#/parameters/Offset"
responses:
200:
description: Data retrieved.
headers:
X-Count:
type: integer
description: |
Total number of managed channels. This value can be used to
implement the paging strategy on API clients.
schema:
$ref: "#/definitions/ChannelList"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/channels/{chanId}:
get:
summary: Retrieves channel info
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/ChannelRes"
403:
description: Missing or invalid access token provided.
404:
description: Channel does not exist.
500:
$ref: "#/responses/ServiceError"
put:
summary: Updates channel info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the channel's ID will not be
affected.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
- name: channel
description: JSON-formatted document describing the updated channel.
in: body
schema:
$ref: "#/definitions/ChannelReq"
required: true
responses:
200:
description: Channel updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Channel does not exist.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Removes a channel
description: |
Removes a channel. The service will ensure that the subscribed apps and
devices are unsubscribed from the removed channel.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
responses:
204:
description: Channel removed.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/channels/{chanId}/clients/{clientId}:
put:
summary: Connects the client to the channel
description: |
Creates connection between a client and a channel. Once connected to
the channel, clients are allowed to exchange messages through it.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
- $ref: "#/parameters/ClientId"
responses:
200:
description: Client connected.
403:
description: Missing or invalid access token provided.
404:
description: Channel or client does not exist.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Disconnects the client from the channel
description: |
Removes connection between a client and a channel. Once connection is
removed, client can no longer exchange messages through the channel.
tags:
- channels
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ChanId"
- $ref: "#/parameters/ClientId"
responses:
204:
description: Client disconnected.
403:
description: Missing or invalid access token provided.
404:
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
description: User's access token.
in: header
type: string
required: true
ChanId:
name: chanId
description: Unique channel identifier.
in: path
type: string
format: uuid
required: true
ClientId:
name: clientId
description: Unique client identifier.
in: path
type: string
format: uuid
required: true
Size:
name: size
description: Size of the subset to retrieve.
in: query
type: integer
default: 10
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
type: integer
default: 0
required: false
responses:
ServiceError:
description: Unexpected server-side error occured.
definitions:
ChannelList:
type: object
properties:
channels:
type: array
minItems: 0
uniqueItems: true
items:
type: object
properties:
id:
type: string
description: Unique channel identifier generated by the service.
name:
type: string
description: Free-form channel name.
required:
- id
required:
- channels
ChannelRes:
type: object
properties:
id:
type: string
description: Unique channel identifier generated by the service.
name:
type: string
description: Free-form channel name.
connected:
type: array
minItems: 0
uniqueItems: true
items:
$ref: '#/definitions/ClientRes'
required:
- id
ChannelReq:
type: object
properties:
name:
type: string
description: Free-form channel name.
ClientList:
type: object
properties:
clients:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/definitions/ClientRes"
required:
- clients
ClientRes:
type: object
properties:
id:
type: string
description: Unique client identifier generated by the service.
type:
type: string
enum:
- app
- device
description: Type of the client.
name:
type: string
description: Free-form client name.
key:
type: string
description: Auto-generated access key.
payload:
type: string
description: Arbitrary, string-encoded client's data.
required:
- id
- type
- key
ClientReq:
type: object
properties:
type:
type: string
enum:
- app
- device
description: Type of the client.
name:
type: string
description: Free-form client name.
payload:
type: string
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
+33
View File
@@ -0,0 +1,33 @@
package manager
import "github.com/asaskevich/govalidator"
// User represents a Mainflux user account. Each user is identified given its
// email and password.
type User struct {
Email string `gorm:"type:varchar(254);primary_key"`
Password string `gorm:"type:char(60)"`
}
// Validate returns an error if user representation is invalid.
func (u *User) Validate() error {
if u.Email == "" || u.Password == "" {
return ErrMalformedEntity
}
if !govalidator.IsEmail(u.Email) {
return ErrMalformedEntity
}
return nil
}
// UserRepository specifies an account persistence API.
type UserRepository interface {
// Save persists the user account. A non-nil error is returned to indicate
// operation failure.
Save(User) error
// One retrieves user by its unique identifier (i.e. email).
One(string) (User, error)
}
+34
View File
@@ -0,0 +1,34 @@
package mainflux
// Message represents a resolved (normalized) raw message.
type Message struct {
Channel string
Publisher string
Protocol string
Name string `json:"n,omitempty"`
Unit string `json:"u,omitempty"`
Value float64 `json:"v,omitempty"`
StringValue string `json:"vs,omitempty"`
BoolValue bool `json:"vb,omitempty"`
DataValue string `json:"vd,omitempty"`
ValueSum float64 `json:"s,omitempty"`
Time float64 `json:"t,omitempty"`
UpdateTime float64 `json:"ut,omitempty"`
Link string `json:"l,omitempty"`
}
// RawMessage represents a message emitted by the mainflux adapters layer.
type RawMessage struct {
Channel string `json:"channel"`
Publisher string `json:"publisher"`
Protocol string `json:"protocol"`
ContentType string `json:"content_type"`
Payload []byte `json:"payload"`
}
// MessagePublisher specifies a message publishing API.
type MessagePublisher interface {
// Publishes message to the stream. A non-nil error is returned to indicate
// operation failure.
Publish(RawMessage) error
}
+47
View File
@@ -0,0 +1,47 @@
# Message normalizer
Normalizer service consumes events published by adapters, normalizes SenML-formatted
ones, and publishes them to the post-processing stream.
## 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_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_NORMALIZER_PORT | Normalizer service HTTP port | 8180 |
## 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:
manager:
image: mainflux/normalizer:[version]
container_name: [instance name]
environment:
MF_NATS_URL: [NATS instance URL]
MF_NORMALIZER_PORT: [Service HTTP port]
```
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/normalizer
# 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
# set the environment variables and run the service
MF_NATS_URL=[NATS instance URL] MF_NORMALIZER_PORT=[Service HTTP port] app
```
+18
View File
@@ -0,0 +1,18 @@
package normalizer
import (
"net/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler() http.Handler {
r := bone.New()
r.GetFunc("/version", mainflux.Version())
r.Handle("/metrics", promhttp.Handler())
return r
}
+3
View File
@@ -0,0 +1,3 @@
// Package normalizer contains the domain concept definitions needed to
// support Mainflux normalizer service functionality.
package normalizer
+112
View File
@@ -0,0 +1,112 @@
package normalizer
import (
"encoding/json"
"fmt"
"github.com/cisco/senml"
"github.com/go-kit/kit/log"
"github.com/mainflux/mainflux"
nats "github.com/nats-io/go-nats"
)
const (
queue string = "normalizers"
subject string = "src.*"
output string = "normalized"
)
type eventFlow struct {
nc *nats.Conn
logger log.Logger
}
// Subscribe instantiates and starts a new NATS message flow.
func Subscribe(nc *nats.Conn, logger log.Logger) {
flow := eventFlow{nc, logger}
flow.start()
}
func (ef eventFlow) start() {
ef.nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
msg := mainflux.RawMessage{}
if err := json.Unmarshal(m.Data, &msg); err != nil {
ef.logger.Log("error", fmt.Sprintf("Unmarshalling failed: %s", err))
return
}
if err := ef.publish(msg); err != nil {
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
return
}
})
}
func (ef eventFlow) publish(msg mainflux.RawMessage) error {
normalized, err := ef.normalize(msg)
if err != nil {
ef.logger.Log("error", fmt.Sprintf("Normalization failed: %s", err))
return err
}
for _, v := range normalized {
data, err := json.Marshal(v)
if err != nil {
ef.logger.Log("error", fmt.Sprintf("Marshalling failed: %s", err))
return err
}
if err = ef.nc.Publish(output, data); err != nil {
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
return err
}
}
return nil
}
func (ef eventFlow) normalize(msg mainflux.RawMessage) ([]mainflux.Message, error) {
var (
raw, normalized senml.SenML
err error
)
if raw, err = senml.Decode(msg.Payload, senml.JSON); err != nil {
return nil, err
}
normalized = senml.Normalize(raw)
msgs := make([]mainflux.Message, len(normalized.Records))
for k, v := range normalized.Records {
m := mainflux.Message{
Channel: msg.Channel,
Publisher: msg.Publisher,
Protocol: msg.Protocol,
Name: v.Name,
Unit: v.Unit,
StringValue: v.StringValue,
DataValue: v.DataValue,
Time: v.Time,
UpdateTime: v.UpdateTime,
Link: v.Link,
}
if v.Value != nil {
m.Value = *v.Value
}
if v.BoolValue != nil {
m.BoolValue = *v.BoolValue
}
if v.Sum != nil {
m.ValueSum = *v.Sum
}
msgs[k] = m
}
return msgs, nil
}
-292
View File
@@ -1,292 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala Notifiers service
description: |
HTTP API for Notifiers service.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.5
servers:
- url: http://localhost:9014
- url: https://localhost:9014
- url: http://localhost:9015
- url: https://localhost:9015
tags:
- name: notifiers
description: Everything about your Notifiers
externalDocs:
description: Find out more about notifiers
url: https://magistrala.absmach.eu/docs/
paths:
/subscriptions:
post:
operationId: createSubscription
summary: Create subscription
description: Creates a new subscription give a topic and contact.
tags:
- notifiers
requestBody:
$ref: "#/components/requestBodies/Create"
responses:
"201":
$ref: "#/components/responses/Create"
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"409":
description: Failed due to using an existing topic and contact.
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: listSubscriptions
summary: List subscriptions
description: List subscriptions given list parameters.
tags:
- notifiers
parameters:
- $ref: "#/components/parameters/Topic"
- $ref: "#/components/parameters/Contact"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Limit"
responses:
"200":
$ref: "#/components/responses/Page"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: A non-existent entity request.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/subscriptions/{id}:
get:
operationId: viewSubscription
summary: Get subscription with the provided id
description: Retrieves a subscription with the provided id.
tags:
- notifiers
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/View"
"400":
description: Failed due to malformed ID.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: A non-existent entity request.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: removeSubscription
summary: Delete subscription with the provided id
description: Removes a subscription with the provided id.
tags:
- notifiers
parameters:
- $ref: "#/components/parameters/Id"
responses:
"204":
description: Subscription removed
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"404":
description: A non-existent entity request.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
Subscription:
type: object
properties:
id:
type: string
format: ulid
example: 01EWDVKBQSG80B6PQRS9PAAY35
description: ULID id of the subscription.
owner_id:
type: string
format: uuid
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
description: An id of the owner who created subscription.
topic:
type: string
example: topic/subtopic
description: Topic to which the user subscribes.
contact:
type: string
example: user@example.com
description: The contact of the user to which the notification will be sent.
CreateSubscription:
type: object
properties:
topic:
type: string
example: topic/subtopic
description: Topic to which the user subscribes.
contact:
type: string
example: user@example.com
description: The contact of the user to which the notification will be sent.
Page:
type: object
properties:
subscriptions:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/Subscription"
total:
type: integer
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
description: Maximum number of items to return in one page.
parameters:
Id:
name: id
description: Unique identifier.
in: path
schema:
type: string
format: ulid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Topic:
name: topic
description: Topic name.
in: query
schema:
type: string
required: false
Contact:
name: contact
description: Subscription contact.
in: query
schema:
type: string
required: false
requestBodies:
Create:
description: JSON-formatted document describing the new subscription to be created
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSubscription"
responses:
Create:
description: Created a new subscription.
headers:
Location:
content:
text/plain:
schema:
type: string
description: Created subscription relative URL
example: /subscriptions/{id}
View:
description: View subscription.
content:
application/json:
schema:
$ref: "#/components/schemas/Subscription"
links:
delete:
operationId: removeSubscription
parameters:
id: $response.body#/id
Page:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Page"
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
security:
- bearerAuth: []
-314
View File
@@ -1,314 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala reader service
description: |
HTTP API for reading messages.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.5
servers:
- url: http://localhost:9003
- url: https://localhost:9003
- url: http://localhost:9005
- url: https://localhost:9005
- url: http://localhost:9009
- url: https://localhost:9009
- url: http://localhost:9011
- url: https://localhost:9011
tags:
- name: readers
description: Everything about your Readers
externalDocs:
description: Find out more about readers
url: https://magistrala.absmach.eu/docs/
paths:
/{domainID}/channels/{chanId}/messages:
get:
operationId: getMessages
summary: Retrieves messages sent to single channel
description: |
Retrieves a list of messages sent to specific channel. Due to
performance concerns, data is retrieved in subsets. The API readers must
ensure that the entire dataset is consumed either by making subsequent
requests, or by increasing the subset size of the initial request.
tags:
- readers
parameters:
- $ref: "#/components/parameters/DomainID"
- $ref: "#/components/parameters/ChanId"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Publisher"
- $ref: "#/components/parameters/Name"
- $ref: "#/components/parameters/Value"
- $ref: "#/components/parameters/BoolValue"
- $ref: "#/components/parameters/StringValue"
- $ref: "#/components/parameters/DataValue"
- $ref: "#/components/parameters/From"
- $ref: "#/components/parameters/To"
- $ref: "#/components/parameters/Aggregation"
- $ref: "#/components/parameters/Interval"
responses:
"200":
$ref: "#/components/responses/MessagesPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"403":
description: Failed to perform authorization over the entity.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
operationId: health
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
MessagesPage:
type: object
properties:
total:
type: number
description: Total number of items that are present on the system.
offset:
type: number
description: Number of items that were skipped during retrieval.
limit:
type: number
description: Size of the subset that was retrieved.
messages:
type: array
minItems: 0
uniqueItems: true
items:
type: object
properties:
channel:
type: integer
description: Unique channel id.
publisher:
type: integer
description: Unique publisher id.
protocol:
type: string
description: Protocol name.
name:
type: string
description: Measured parameter name.
unit:
type: string
description: Value unit.
value:
type: number
description: Measured value in number.
stringValue:
type: string
description: Measured value in string format.
boolValue:
type: boolean
description: Measured value in boolean format.
dataValue:
type: string
description: Measured value in binary format.
valueSum:
type: number
description: Sum value.
time:
type: number
description: Time of measurement.
updateTime:
type: number
description: Time of updating measurement.
parameters:
DomainID:
name: domainID
description: Unique domain identifier.
in: path
schema:
type: string
format: uuid
required: true
ChanId:
name: chanId
description: Unique channel identifier.
in: path
schema:
type: string
format: uuid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Publisher:
name: Publisher
description: Unique thing identifier.
in: query
schema:
type: string
format: uuid
required: false
Name:
name: name
description: SenML message name.
in: query
schema:
type: string
required: false
Value:
name: v
description: SenML message value.
in: query
schema:
type: string
required: false
BoolValue:
name: vb
description: SenML message bool value.
in: query
schema:
type: boolean
required: false
StringValue:
name: vs
description: SenML message string value.
in: query
schema:
type: string
required: false
DataValue:
name: vd
description: SenML message data value.
in: query
schema:
type: string
required: false
Comparator:
name: comparator
description: Value comparison operator.
in: query
schema:
type: string
default: eq
enum:
- eq
- lt
- le
- gt
- ge
required: false
From:
name: from
description: SenML message time in nanoseconds (integer part represents seconds).
in: query
schema:
type: number
example: 1709218556069
required: false
To:
name: to
description: SenML message time in nanoseconds (integer part represents seconds).
in: query
schema:
type: number
example: 1709218757503
required: false
Aggregation:
name: aggregation
description: Aggregation function.
in: query
schema:
type: string
enum:
- MAX
- AVG
- MIN
- SUM
- COUNT
- max
- min
- sum
- avg
- count
example: MAX
required: false
Interval:
name: interval
description: Aggregation interval.
in: query
schema:
type: string
example: 10s
required: false
responses:
MessagesPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/MessagesPage"
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
thingAuth:
type: http
scheme: bearer
bearerFormat: uuid
description: |
* Things access: "Authorization: Thing <thing_key>"
security:
- bearerAuth: []
- thingAuth: []
-553
View File
@@ -1,553 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala Reports Service API
description: |
HTTP API for managing reports service.
version: 0.18.5
servers:
- url: http://localhost:9017
tags:
- name: reports
description: Operations related to report configurations and generation
paths:
/{domainID}/reports:
post:
operationId: generateReport
summary: Generate a report
description: Generates a report based on the provided configuration or an existing config. The action determines the response format.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateReportRequest'
responses:
'200':
description: Report generated successfully (content varies by action)
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateReportResponse'
application/octet-stream:
schema:
type: string
format: binary
'400':
description: Invalid request parameters
'401':
description: Missing or invalid access token
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/reports/configs:
post:
operationId: addReportConfig
summary: Create a report configuration
description: Creates a new report configuration.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AddReportConfigRequest'
responses:
'201':
description: Report configuration created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/ReportConfig'
'400':
description: Invalid request body
'401':
description: Missing or invalid access token
'500':
$ref: '#/components/responses/ServiceError'
get:
operationId: listReportConfigs
summary: List report configurations
description: Retrieves a paginated list of report configurations.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
security:
- bearerAuth: []
responses:
'200':
description: List of report configurations
content:
application/json:
schema:
$ref: '#/components/schemas/ListReportsConfigResponse'
'400':
description: Invalid query parameters
'401':
description: Missing or invalid access token
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/reports/configs/{reportID}:
get:
operationId: viewReportConfig
summary: View a report configuration
description: Retrieves details of a specific report configuration.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/ReportID'
security:
- bearerAuth: []
responses:
'200':
description: Report configuration details
content:
application/json:
schema:
$ref: '#/components/schemas/ReportConfig'
'404':
description: Report configuration not found
'401':
description: Missing or invalid access token
'500':
$ref: '#/components/responses/ServiceError'
patch:
operationId: updateReportConfig
summary: Update a report configuration
description: Updates specified fields of a report configuration.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/ReportID'
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateReportConfigRequest'
responses:
'200':
description: Report configuration updated
content:
application/json:
schema:
$ref: '#/components/schemas/ReportConfig'
'400':
description: Invalid request body
'401':
description: Missing or invalid access token
'404':
description: Report configuration not found
'500':
$ref: '#/components/responses/ServiceError'
delete:
operationId: deleteReportConfig
summary: Delete a report configuration
description: Permanently deletes a report configuration.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/ReportID'
security:
- bearerAuth: []
responses:
'204':
description: Report configuration deleted
'401':
description: Missing or invalid access token
'404':
description: Report configuration not found
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/reports/configs/{reportID}/schedule:
patch:
operationId: updateReportSchedule
summary: Update report schedule
description: Updates the schedule of a report configuration.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/ReportID'
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Schedule'
responses:
'200':
description: Schedule updated
content:
application/json:
schema:
$ref: '#/components/schemas/ReportConfig'
'400':
description: Invalid schedule
'401':
description: Missing or invalid access token
'404':
description: Report configuration not found
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/reports/configs/{reportID}/enable:
post:
operationId: enableReportConfig
summary: Enable a report configuration
description: Enables a report configuration to generate scheduled reports.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/ReportID'
security:
- bearerAuth: []
responses:
'200':
description: Report configuration enabled
content:
application/json:
schema:
$ref: '#/components/schemas/ReportConfig'
'401':
description: Missing or invalid access token
'404':
description: Report configuration not found
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/reports/configs/{reportID}/disable:
post:
operationId: disableReportConfig
summary: Disable a report configuration
description: Disables a report configuration, stopping scheduled reports.
tags:
- reports
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/ReportID'
security:
- bearerAuth: []
responses:
'200':
description: Report configuration disabled
content:
application/json:
schema:
$ref: '#/components/schemas/ReportConfig'
'401':
description: Missing or invalid access token
'404':
description: Report configuration not found
'500':
$ref: '#/components/responses/ServiceError'
/health:
get:
summary: Service health check
tags:
- health
responses:
'200':
$ref: '#/components/responses/HealthRes'
components:
schemas:
ReportConfig:
type: object
properties:
id:
type: string
readOnly: true
name:
type: string
description:
type: string
domain_id:
type: string
readOnly: true
schedule:
$ref: '#/components/schemas/Schedule'
config:
$ref: '#/components/schemas/MetricConfig'
email:
$ref: '#/components/schemas/EmailSetting'
metrics:
type: array
items:
$ref: '#/components/schemas/ReqMetric'
status:
$ref: '#/components/schemas/Status'
created_at:
type: string
format: date-time
readOnly: true
created_by:
type: string
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
updated_by:
type: string
readOnly: true
required:
- name
- metrics
- config
Schedule:
type: object
properties:
recurring:
type: string
enum: [None, Daily, Weekly, Monthly]
recurring_period:
type: integer
minimum: 1
start_time:
type: string
format: date-time
next_run:
type: string
format: date-time
readOnly: true
MetricConfig:
type: object
properties:
title:
type: string
maxLength: 100
format:
type: string
enum: [pdf, csv, html]
aggregation:
$ref: '#/components/schemas/AggConfig'
AggConfig:
type: object
properties:
window:
type: string
function:
type: string
enum: [sum, average, max, min]
EmailSetting:
type: object
properties:
recipients:
type: array
items:
type: string
format: email
subject:
type: string
body_template:
type: string
required:
- recipients
- subject
ReqMetric:
type: object
properties:
name:
type: string
type:
type: string
enum: [gauge, counter, histogram]
parameters:
type: object
required:
- name
- type
Status:
type: string
enum: [enabled, disabled]
GenerateReportRequest:
type: object
properties:
action:
type: string
enum: [view, download, email]
config_id:
type: string
name:
type: string
description:
type: string
schedule:
$ref: '#/components/schemas/Schedule'
config:
$ref: '#/components/schemas/MetricConfig'
email:
$ref: '#/components/schemas/EmailSetting'
metrics:
type: array
items:
$ref: '#/components/schemas/ReqMetric'
required:
- action
GenerateReportResponse:
type: object
properties:
total:
type: integer
from:
type: string
format: date-time
to:
type: string
format: date-time
aggregation:
$ref: '#/components/schemas/AggConfig'
reports:
type: array
items:
$ref: '#/components/schemas/Report'
Report:
type: object
properties:
timestamp:
type: string
format: date-time
value:
type: number
metric_name:
type: string
AddReportConfigRequest:
type: object
properties:
name:
type: string
description:
type: string
schedule:
$ref: '#/components/schemas/Schedule'
config:
$ref: '#/components/schemas/MetricConfig'
email:
$ref: '#/components/schemas/EmailSetting'
metrics:
type: array
items:
$ref: '#/components/schemas/ReqMetric'
status:
$ref: '#/components/schemas/Status'
required:
- name
- metrics
- config
UpdateReportConfigRequest:
type: object
properties:
name:
type: string
description:
type: string
schedule:
$ref: '#/components/schemas/Schedule'
config:
$ref: '#/components/schemas/MetricConfig'
email:
$ref: '#/components/schemas/EmailSetting'
metrics:
type: array
items:
$ref: '#/components/schemas/ReqMetric'
status:
$ref: '#/components/schemas/Status'
ListReportsConfigResponse:
type: object
properties:
total:
type: integer
offset:
type: integer
limit:
type: integer
report_configs:
type: array
items:
$ref: '#/components/schemas/ReportConfig'
parameters:
DomainID:
name: domainID
in: path
required: true
schema:
type: string
ReportID:
name: reportID
in: path
required: true
schema:
type: string
Offset:
name: offset
in: query
schema:
type: integer
default: 0
minimum: 0
Limit:
name: limit
in: query
schema:
type: integer
default: 10
minimum: 1
maximum: 100
responses:
ServiceError:
description: Unexpected server error
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: './schemas/health_info.yaml'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
-586
View File
@@ -1,586 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala Rules Engine API
description: |
HTTP API for managing rules engine service.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.5
servers:
- url: http://localhost:9008
- url: http://localhost:9008
tags:
- name: rules engine
description: Everything about your Rules Engine
externalDocs:
description: Find out more about rules engine
url: https://magistrala.absmach.eu/docs/
paths:
/{domainID}/rules:
post:
operationId: createRule
summary: Create Rule
description: |
Creates a new rule for message processing
tags:
- rules
parameters:
- $ref: '#/components/parameters/DomainID'
security:
- bearerAuth: []
requestBody:
$ref: '#/components/requestBodies/RuleCreateReq'
responses:
'201':
$ref: '#/components/responses/RuleCreateRes'
'400':
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
'415':
description: Missing or invalid content type
"500":
$ref: "#/components/responses/ServiceError"
"503":
description: Failed to receive response from the clients service.
get:
operationId: getRules
summary: List Rules
description: |
Retrieves a list of rules with optional filtering
tags:
- rules
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/InputChannel'
- $ref: '#/components/parameters/OutputChannel'
- $ref: '#/components/parameters/Status'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/RuleListRes'
'400':
description: Failed due to malformed query parameters
'401':
description: Missing or invalid access token
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/rules/{ruleID}:
get:
operationId: getRule
summary: View Rule
description: Retrieves a rule by ID
tags:
- rules
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/RuleID'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/RuleRes'
"400":
description: Missing or invalid rule
"403":
description: Failed to perform authorization over the entity
'401':
description: Missing or invalid access token
'404':
description: Rule does not exist
"422":
description: Database can't process request
"500":
$ref: "#/components/responses/ServiceError"
put:
operationId: updateRule
summary: Update Rule
description: Updates an existing rule
tags:
- rules
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/RuleID'
security:
- bearerAuth: []
requestBody:
$ref: '#/components/requestBodies/RuleUpdateReq'
responses:
'200':
$ref: '#/components/responses/RuleRes'
'400':
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
'404':
description: Rule does not exist
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: removeRule
summary: Delete Rule
description: Deletes a rule
tags:
- rules
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/RuleID'
security:
- bearerAuth: []
responses:
'204':
description: Rule deleted successfully
"400":
description: Failed due to malformed rule ID
'401':
description: Missing or invalid access token
"403":
description: Failed to perform authorization over the entity
"422":
description: Database can't process request
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/rules/{ruleID}/enable:
put:
operationId: enableRule
summary: Enable Rule
description: Enables a rule for processing
tags:
- rules
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/RuleID'
security:
- bearerAuth: []
responses:
'200':
description: Rule enabled successfully
"400":
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
"403":
description: Failed to perform authorization over the entity
'404':
description: Rule does not exist
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/rules/{ruleID}/disable:
put:
operationId: disableRule
summary: Disable Rule
description: Disables a rule from processing
tags:
- Rules
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/RuleID'
security:
- bearerAuth: []
responses:
'200':
description: Rule disabled successfully
"400":
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
"403":
description: Failed to perform authorization over the entity
'404':
description: Rule does not exist
"422":
description: Database can't process request
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
RulesListRes:
type: object
properties:
total:
type: integer
description: Total number of results
minimum: 0
offset:
type: integer
description: Number of items to skip during retrieval
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve
maximum: 100
default: 10
rules:
type: array
minItems: 0
uniqueItems: true
items:
$ref: '#/components/schemas/Rule'
required:
- rules
Rule:
type: object
properties:
id:
type: string
description: Unique rule identifier
name:
type: string
description: Rule name
domain:
type: string
description: Domain ID this rule belongs to
metadata:
type: object
description: Custom metadata
additionalProperties:
type: string
input_channel:
type: string
description: Input channel for receiving messages
input_topic:
type: string
description: Input topic for receiving messages
logic:
type: object
description: Rule processing logic script
properties:
script:
type: string
description: Script content
output_channel:
type: string
description: Output channel for processed messages
output_topic:
type: string
description: Output topic for processed messages
schedule:
type: object
description: Rule execution schedule
properties:
start_datetime:
type: string
format: date-time
description: When the schedule becomes active
time:
type: string
format: date-time
description: Specific time for the rule to run
recurring:
type: string
description: Schedule recurrence pattern
enum: [None, Daily, Weekly, Monthly]
recurring_period:
type: integer
minimum: 1
description: Controls how many intervals to skip between executions (1 = every interval, 2 = every second interval, etc.)
status:
type: string
description: Rule status
enum: [enabled, disabled]
created_at:
type: string
format: date-time
description: Creation timestamp
readOnly: true
created_by:
type: string
description: User who created the rule
updated_at:
type: string
format: date-time
description: Last update timestamp
readOnly: true
updated_by:
type: string
description: User who last updated the rule
required:
- name
- domain
- input_channel
- input_topic
- logic
- status
parameters:
DomainID:
name: domainID
description: Domain ID
in: path
required: true
schema:
type: string
RuleID:
name: ruleID
description: Rule ID
in: path
required: true
schema:
type: string
Offset:
name: offset
description: Number of items to skip
in: query
required: false
schema:
type: integer
default: 0
minimum: 0
Limit:
name: limit
description: Size of the subset
in: query
required: false
schema:
type: integer
default: 10
minimum: 1
InputChannel:
name: input_channel
description: Filter by input channel
in: query
required: false
schema:
type: string
OutputChannel:
name: output_channel
description: Filter by output channel
in: query
required: false
schema:
type: string
Status:
name: status
description: Filter by rule status
in: query
required: false
schema:
type: string
enum: [enabled, disabled]
default: enabled
requestBodies:
RuleCreateReq:
description: JSON-formatted document describing the new rule
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Rule name
domain:
type: string
description: Domain ID this rule belongs to
metadata:
type: object
description: Custom metadata
additionalProperties:
type: string
input_channel:
type: string
description: Input channel for receiving messages
input_topic:
type: string
description: Input topic for receiving messages
logic:
type: object
description: Rule processing logic script
properties:
script:
type: string
description: Script content
output_channel:
type: string
description: Output channel for processed messages
output_topic:
type: string
description: Output topic for processed messages
schedule:
type: object
description: Rule execution schedule
properties:
start_datetime:
type: string
format: date-time
description: When the schedule becomes active
time:
type: string
format: date-time
description: Specific time for the rule to run
recurring:
type: string
description: Schedule recurrence pattern
enum: [None, Daily, Weekly, Monthly]
recurring_period:
type: integer
minimum: 1
description: Controls how many intervals to skip between executions
status:
type: string
description: Rule status
enum: [enabled, disabled]
required:
- name
- domain
- input_channel
- input_topic
- logic
- schedule
RuleUpdateReq:
description: JSON-formatted document describing the rule update
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Rule name
metadata:
type: object
description: Custom metadata
additionalProperties:
type: string
input_channel:
type: string
description: Input channel for receiving messages
input_topic:
type: string
description: Input topic for receiving messages
logic:
type: object
description: Rule processing logic script
properties:
script:
type: string
description: Script content
output_channel:
type: string
description: Output channel for processed messages
output_topic:
type: string
description: Output topic for processed messages
schedule:
type: object
description: Rule execution schedule
properties:
start_datetime:
type: string
format: date-time
description: When the schedule becomes active
time:
type: string
format: date-time
description: Specific time for the rule to run
recurring:
type: string
description: Schedule recurrence pattern
enum: [None, Daily, Weekly, Monthly]
recurring_period:
type: integer
minimum: 1
description: Controls how many intervals to skip between executions
status:
type: string
description: Rule status
enum: [enabled, disabled]
responses:
RuleCreateRes:
description: Rule registered
headers:
Location:
content:
text/plain:
schema:
type: string
description: Created rule's relative URL (i.e. /rules/{ruleID})
RuleListRes:
description: Data retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/RulesListRes'
RuleRes:
description: Data retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/Rule'
links:
update:
operationId: updateRule
parameters:
ruleID: $response.body#/id
enable:
operationId: enableRule
parameters:
ruleID: $response.body#/id
disable:
operationId: disableRule
parameters:
ruleID: $response.body#/id
delete:
operationId: removeRule
parameters:
ruleID: $response.body#/id
ServiceError:
description: Unexpected server-side error occurred
HealthRes:
description: Service Health Check
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
-30
View File
@@ -1,30 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
type: object
properties:
status:
type: string
description: Service status.
enum:
- pass
version:
type: string
description: Service version.
example: v0.18.0
commit:
type: string
description: Service commit hash.
example: 73362210dd2e04e389eaddb802cab3fe03976593
description:
type: string
description: Service description.
example: <service_name> service
build_time:
type: string
description: Service build time.
example: 2024-02-01_12:18:15
instance_id:
type: string
description: Service instance ID.
example: 8edbf8af-7db7-4218-bb4f-a8a929ff5266
-395
View File
@@ -1,395 +0,0 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
components:
schemas:
NewRole:
type: object
properties:
id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Role unique identifier.
name:
type: string
description: Role's name.
example: "roleName"
entity_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Entity unique identifier.
created_by:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Role creator unique identifier.
created_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the channel was created.
updated_by:
type: string
format: uuid
example: ""
description: Role updater unique identifier.
updated_at:
type: string
format: date-time
example: ""
description: Time when the channel was created.
Role:
type: object
properties:
id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Role unique identifier.
name:
type: string
description: Role's name.
example: "roleName"
entity_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Entity unique identifier.
created_by:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Role creator unique identifier.
created_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the channel was created.
updated_by:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Role updater unique identifier.
updated_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the channel was created.
CreateRoleObj:
type: object
properties:
name:
type: string
description: Role's name.
example: "roleName"
optional_actions:
type: array
description: List of optional actions.
items:
type: string
example:
[
"read",
"update",
]
optional_members:
type: array
minItems: 0
description: List of optional members.
items:
type: string
example:
[
"5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb",
"c01ed106-e52d-4aa4-bed3-39f360177cfa",
]
UpdateRoleObj:
type: object
properties:
name:
type: string
description: Role's name.
RolesPage:
type: object
properties:
roles:
type: array
items:
$ref: '#/components/schemas/Role'
description: List of roles.
total:
type: integer
description: Total number of roles.
example: 1
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
example: 10
description: Maximum number of items to return in one page.
EntityMembersPage:
type: object
properties:
groups:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/components/schemas/EntityMembersObj"
total:
type: integer
example: 1
description: Total number of items.
offset:
type: integer
description: Number of items to skip during retrieval.
limit:
type: integer
example: 10
description: Maximum number of items to return in one page.
required:
- groups
- total
- offset
RoleActionsObj:
type: object
properties:
actions:
type: array
description: List of actions to be added to a role.
items:
type: string
example:
[
"read",
"update",
]
RoleMembersObj:
type: object
properties:
members:
type: array
description: List of members to be added to a role.
items:
type: string
example:
[
"5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb",
"c01ed106-e52d-4aa4-bed3-39f360177cfa",
]
EntityMembersObj:
type: object
properties:
members:
type: array
description: List of members with assigned roles and actions.
items:
type: object
properties:
id:
type: string
format: uuid
description: Unique identifier of the member.
roles:
type: array
description: List of roles assigned to the member.
items:
type: object
properties:
id:
type: string
format: uuid
description: Unique identifier of the role.
name:
type: string
description: Name of the role.
actions:
type: array
description: List of actions the member can perform.
items:
type: string
access_type:
type: string
description: Type of access granted.
enum: [read, write, admin] # Adjust based on your actual access types.
example:
members:
- id: "5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb"
roles:
- id: "a1b2c3d4-e5f6-7890-1234-56789abcdef0"
name: "Admin"
actions: ["create", "update", "delete"]
access_type: "admin"
- id: "c01ed106-e52d-4aa4-bed3-39f360177cfa"
roles:
- id: "b2c3d4e5-f678-9012-3456-789abcdef012"
name: "Editor"
actions: ["read", "update"]
access_type: "write"
AvailableActionsObj:
type: object
properties:
available_actions:
type: array
description: List of all available actions.
items:
type: string
example:
[
"read",
"update",
]
parameters:
RoleName:
name: roleName
description: Role's name.
in: path
schema:
type: string
required: true
example: "roleName"
RoleID:
name: roleID
description: Role ID.
in: path
schema:
type: string
required: true
example: "roleID"
RoleIDQuery:
name: role_id
description: Filter by role ID.
in: query
schema:
type: string
format: uuid
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
RoleNameQuery:
name: role_name
description: Filter by role name.
in: query
schema:
type: string
required: false
example: admin
ActionsQuery:
name: actions
description: Filter by actions. Multiple actions can be specified separated by comma.
in: query
schema:
type: string
required: false
example: "action1,action2"
requestBodies:
CreateRoleReq:
description: JSON- formatted object decsribing a new role to be created.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRoleObj'
UpdateRoleReq:
description: JSON- formatted object decsribing a role to be updated.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateRoleObj'
AddRoleActionsReq:
description: JSON- formatted object decsribing an action to be added to a role.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RoleActionsObj'
AddRoleMembersReq:
description: JSON- formatted object decsribing a member to be added to a role.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RoleMembersObj'
responses:
CreateRoleRes:
description: Role created successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/NewRole'
ListRolesRes:
description: Role retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/RolesPage'
GetRoleRes:
description: Role retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/Role'
AddRoleActionsRes:
description: Action added to role successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/RoleActionsObj'
ListRoleActionsRes:
description: Role actions retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/RoleActionsObj'
AddRoleMembersRes:
description: Member added to role successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/RoleMembersObj'
ListRoleMembersRes:
description: Role members retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/RoleMembersObj'
ListAvailableActionsRes:
description: Available actions retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/AvailableActionsObj'
ListEntityMembersRes:
description: Entity members retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/EntityMembersObj'
-1727
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
package mainflux
import "os"
// Env reads specified environment variable. If no value has been found,
// fallback is returned.
func Env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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