mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
438 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50e14f0c65 | |||
| d2bce84c06 | |||
| 89aa9603ec | |||
| 2ae581368f | |||
| 99a3ec5d2d | |||
| 89a0fa3e9c | |||
| 1bc4dc9575 | |||
| bca3d0774f | |||
| 816c172823 | |||
| 6a361209c8 | |||
| 27e1be23f5 | |||
| c9bf245010 | |||
| ff30957614 | |||
| db6165aa6b | |||
| c088e664c3 | |||
| d499068e43 | |||
| 38d4c3fcea | |||
| aea7db14b7 | |||
| 58f3c735a0 | |||
| 789963447a | |||
| 2dc7f3eea1 | |||
| 62297fedec | |||
| f449f8b9c8 | |||
| 544ba57850 | |||
| e858e86ed9 | |||
| 84679ed42a | |||
| b0610f034c | |||
| cb9c0ee32a | |||
| 3a5f4395e4 | |||
| 36a00f3464 | |||
| 703d0543af | |||
| fb3619645d | |||
| 46a9cc086d | |||
| 367d3edbef | |||
| d5b913d432 | |||
| 2dace5564f | |||
| 88b30626dd | |||
| 301d855015 | |||
| 7a193829f4 | |||
| 523716b090 | |||
| 36ca9015ff | |||
| a6dfc26959 | |||
| 924f6f120a | |||
| 2c1c94af9b | |||
| f5fbd6f22a | |||
| 55bdc029c3 | |||
| e42d422410 | |||
| bf5d1544b6 | |||
| 8876acbfec | |||
| 250505700b | |||
| 5cb169f7f8 | |||
| ccd8965d6f | |||
| b23ecb64e5 | |||
| 03b020ae87 | |||
| 255d1c03b1 | |||
| 7fe8100d6b | |||
| 6d9c92596c | |||
| 3ac88b05a8 | |||
| f84958834e | |||
| 7723b92ba7 | |||
| 3072d054ee | |||
| b4a51ae0a2 | |||
| f277074d42 | |||
| 2f1b999727 | |||
| ce86fbbe90 | |||
| 615ba9267d | |||
| 10113bd354 | |||
| c691ee06f0 | |||
| 23a3a5f194 | |||
| 6f202a9f83 | |||
| 74f890128d | |||
| 6099da59e6 | |||
| 0255a4cd61 | |||
| 411cbf575a | |||
| 5d715aa984 | |||
| 30f7dda491 | |||
| ff5d77722c | |||
| b6eac477b4 | |||
| 7cc03be424 | |||
| 4f467dcdde | |||
| d413508b25 | |||
| 8036c74207 | |||
| 3b57ebf532 | |||
| 15e9745517 | |||
| f30c9725aa | |||
| 4c97094427 | |||
| 253a7afcf7 | |||
| 52ab91c275 | |||
| 5899177f57 | |||
| 3e11147ab5 | |||
| c57c9f0a3c | |||
| b6b997237e | |||
| c11cf5820c | |||
| 28bff54ea6 | |||
| 00117519c4 | |||
| 238394e0a7 | |||
| 7eb12015c0 | |||
| 46a7341eba | |||
| b651537ba9 | |||
| b9e1c0da81 | |||
| 3466776152 | |||
| 3f9f56d0d4 | |||
| 2e5316e3fd | |||
| 9c429d142d | |||
| 11eed2aa2c | |||
| 8019cd731e | |||
| 923a5aefae | |||
| 89015b0292 | |||
| c7054a07f2 | |||
| c2d6cc511c | |||
| 8d4543873f | |||
| 288f437b4d | |||
| 86f563ee10 | |||
| dd874008b2 | |||
| 9cec19896e | |||
| a79a9012e4 | |||
| 7c48c1cd78 | |||
| 13e9b440fe | |||
| 481b5b230d | |||
| da3089cf87 | |||
| eb99ef97e3 | |||
| 1c664a002e | |||
| 23111608c8 | |||
| 199a44b0a0 | |||
| ea3fb4b781 | |||
| 778685fca0 | |||
| df61bdccad | |||
| 82aa89397e | |||
| 0f2020aeee | |||
| 499b44fae2 | |||
| 7721b61fa2 | |||
| f2aa007fab | |||
| 3ecc79375d | |||
| f8c41caeb3 | |||
| 0c92be51c8 | |||
| 6347d6cbf3 | |||
| 0c629cce47 | |||
| 2554adcc1e | |||
| 3494c7a5e1 | |||
| 9afaea24c2 | |||
| 7ea0d444bf | |||
| 6dc67f66da | |||
| c60b124ad2 | |||
| 6f97c4588e | |||
| 75a8cdfe3f | |||
| bdf5001072 | |||
| 40d4dd5f2d | |||
| 4132a5c017 | |||
| 26a2ea1ce1 | |||
| bcc0bfdc3d | |||
| c966a7802d | |||
| 311fe29dc4 | |||
| 623f9b1951 | |||
| e47e5ffc22 | |||
| 3d7c341c5e | |||
| 6fb3c3f594 | |||
| 79a6fc158b | |||
| cc3283c32b | |||
| fe3ecd5c78 | |||
| 763f9aa0b7 | |||
| 0e5f97a8a8 | |||
| 3eeb2ef10e | |||
| eff9f62b8b | |||
| 5cebacdd35 | |||
| 0d3934e9a6 | |||
| 01bd08d5b8 | |||
| 11f28ef754 | |||
| 717da672ac | |||
| f21c36fcf6 | |||
| b7863f171c | |||
| b7c5f14236 | |||
| 25a8d94e8a | |||
| d9ef550697 | |||
| e967cfe5df | |||
| 478d22a0d6 | |||
| d1e450c752 | |||
| 4a9f1b1414 | |||
| 656add84f1 | |||
| 90a141ea41 | |||
| e297ab4607 | |||
| f8ee020c22 | |||
| 67844820c3 | |||
| a473d16cd3 | |||
| 8c2726200f | |||
| e93c447d0a | |||
| 79a6683406 | |||
| bc55890dc0 | |||
| e44b035343 | |||
| 04586424b3 | |||
| 1ce3f72d87 | |||
| 367871d268 | |||
| b8a67898cc | |||
| db361038af | |||
| b71de906b3 | |||
| 43f1f11964 | |||
| 704b523a22 | |||
| 890785baa1 | |||
| d9cad2b4b3 | |||
| c657eedb6c | |||
| 0a825bcce9 | |||
| 5ac6ddb3e3 | |||
| 4fa1f7c6a5 | |||
| 85093c0b4b | |||
| 80edbada70 | |||
| f93c6a4249 | |||
| 49249f08e2 | |||
| 33ff1e7529 | |||
| d9df54046c | |||
| e73132e748 | |||
| 23c8f1f53f | |||
| 811b5b787b | |||
| 4332a5ea4b | |||
| 6e3b8407d9 | |||
| 2f96df1c35 | |||
| 90cdfb300e | |||
| 318292703a | |||
| ce40a0a7a5 | |||
| 9f4acaf7a8 | |||
| 068d3b6602 | |||
| 09c063496b | |||
| 56286610d9 | |||
| 669a3b67e5 | |||
| 3d4d5291c3 | |||
| b84f268e14 | |||
| 637eb34178 | |||
| e0fbc05c4d | |||
| c64337bf03 | |||
| ad91c25082 | |||
| e775c2a94b | |||
| cfa40c70fb | |||
| 691dcbeb80 | |||
| 2a07613c75 | |||
| abd8c9b6c9 | |||
| b60341db1d | |||
| b2417b9bd6 | |||
| ad0278005c | |||
| ff238a3491 | |||
| 15240dc391 | |||
| 4e8e7dbf4e | |||
| e0e17f0e18 | |||
| e389395d4e | |||
| 38ad5172e1 | |||
| 8e98cb5e0c | |||
| 939f2612dd | |||
| 133ca7116a | |||
| a005fd8398 | |||
| 13417691ad | |||
| d71eb8b20f | |||
| f90d043b29 | |||
| d647f62a27 | |||
| 1b938002b5 | |||
| 0e34885c73 | |||
| f58915dd79 | |||
| ed7b640700 | |||
| 9363cb6804 | |||
| 48779e856f | |||
| 20ce5268d4 | |||
| a6aca34769 | |||
| 722381a637 | |||
| a93de22af7 | |||
| 9561949e60 | |||
| a3886c64e9 | |||
| d38ea79dde | |||
| c12a88d12f | |||
| 6b54d38d53 | |||
| ad6f9ee721 | |||
| d0ff035688 | |||
| fa9bc47482 | |||
| b605341f73 | |||
| c9e7fb112a | |||
| 1b2b58f6b0 | |||
| 73f7af3eeb | |||
| cc50880217 | |||
| bbc66553b2 | |||
| 3f0eaaec31 | |||
| 1a6102c5fe | |||
| d2b03d09ae | |||
| 2cd0e70d80 | |||
| 05f0fc776d | |||
| f7a21d43da | |||
| 520006f06d | |||
| 180aaeae6f | |||
| e847ac251a | |||
| 808dbb82df | |||
| 1cf4558d11 | |||
| b1fab11c32 | |||
| 63a97748b6 | |||
| 122179b8c5 | |||
| bb5f4c1196 | |||
| 4f5ef30b30 | |||
| d1c44a5413 | |||
| 1d74046154 | |||
| de236fcc90 | |||
| 9de6bc1448 | |||
| ce928986ba | |||
| 37399c8944 | |||
| 849ce6e1ad | |||
| 96fd2bf2b5 | |||
| 62c43f1ba4 | |||
| 3db4ab8440 | |||
| 5d7d5d43ff | |||
| 2d8ddd7b0c | |||
| 779b254c04 | |||
| f3f2586f03 | |||
| d02009744e | |||
| d38b125fb7 | |||
| 551172ed0a | |||
| 04710ad019 | |||
| 57a4535bc4 | |||
| 52f0017be5 | |||
| 2074bac535 | |||
| 106c8996d4 | |||
| fd0ca0e6d6 | |||
| bd4a97fdbe | |||
| 120acd8564 | |||
| 566d8ea254 | |||
| aa3e23906f | |||
| be956accd6 | |||
| c06d62a8a7 | |||
| 20b0207a3c | |||
| 469b8f0c79 | |||
| f3dfcd7e7e | |||
| b8f938e180 | |||
| bbc45150cc | |||
| d429dec1d8 | |||
| eaf6863d1c | |||
| 0bc2fd8ff6 | |||
| 2afd866ec3 | |||
| 9e64f3a7d3 | |||
| 3ef7035689 | |||
| b7be831b8b | |||
| 6a24aa91e6 | |||
| b88fc346d0 | |||
| fb4140d69c | |||
| f92db09290 | |||
| b76461e773 | |||
| bc54a9ad6a | |||
| 3bb18620b2 | |||
| 2f1e5163b5 | |||
| f06687e2f4 | |||
| 460874b9ab | |||
| fc4c658ff2 | |||
| 8f4ef8464e | |||
| 454760e62e | |||
| 797a804cda | |||
| 8aa4bbc053 | |||
| 0934fbe834 | |||
| 09907fd78f | |||
| dcd45d8bda | |||
| daf9f39a11 | |||
| 1750395240 | |||
| 4c013fac6f | |||
| d950002593 | |||
| 0dca37b656 | |||
| cf17b53bb6 | |||
| 8fee81d8bc | |||
| eff9fd2411 | |||
| 396727b4d5 | |||
| 2fb79f1a19 | |||
| 7d332297fc | |||
| bb0ead885f | |||
| 00c6cb77c6 | |||
| 021427431a | |||
| 129e911652 | |||
| 41c1bc03b4 | |||
| 88405089f6 | |||
| 9e8888fdff | |||
| bd16fa2616 | |||
| 8eee6f1bcf | |||
| fbb527b435 | |||
| b046ce5dd7 | |||
| 08c3bac64e | |||
| f88a913333 | |||
| ef00dac107 | |||
| 87fa44f9c9 | |||
| 7eb4c34ab1 | |||
| f3fd689305 | |||
| 55b28f90f0 | |||
| 40194aeb9b | |||
| b930a8f75f | |||
| 469880a3fa | |||
| 5efcacc731 | |||
| 7382ab9073 | |||
| 0faf6d85c0 | |||
| e2ab3e19e6 | |||
| 980f6ac525 | |||
| e7e4da29bb | |||
| 54913c7d23 | |||
| be79cae443 | |||
| 59da29ff2b | |||
| 744068bda8 | |||
| 122c02769d | |||
| 276a505eeb | |||
| 2e27375adb | |||
| 38311af348 | |||
| 455b469bc2 | |||
| 63281acac5 | |||
| 253495d5bb | |||
| 66ef92dbf7 | |||
| f2fd735dbd | |||
| 4e5edad061 | |||
| cb6471790b | |||
| 5e31f45b27 | |||
| 51898276f3 | |||
| 9f3a728ccb | |||
| f36013016e | |||
| b520216351 | |||
| 92b4a70528 | |||
| fafa623680 | |||
| e6203967bb | |||
| 3b86bf249c | |||
| 700a34b589 | |||
| a3a2c1f449 | |||
| 910791c8fa | |||
| dd806d2a2e | |||
| 83e439bb5a | |||
| 45451c8266 | |||
| 47597331f1 | |||
| 8d914647c8 | |||
| 4c7b046824 | |||
| 46498c152f | |||
| 427d03fa0a | |||
| 1c9c5e6605 | |||
| 014d13298a | |||
| 9d8004ee4c | |||
| a2d57cae38 | |||
| 3e5a93efbf | |||
| c137bda7c4 | |||
| 70805a0b2a | |||
| 8979bb96db | |||
| fc410bd89f | |||
| c8c61bfe4a | |||
| ac018a665c | |||
| 17adff355c | |||
| 38c08359a9 | |||
| 12fc1e4bcc | |||
| 5711803c8d | |||
| 7d5b901dc9 |
@@ -0,0 +1 @@
|
||||
* @mainflux/maintainers
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
|
||||
|
||||
To start contributing to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
|
||||
clone your fork repository, and configure the remotes:
|
||||
|
||||
```
|
||||
git clone https://github.com/<your-username>/mainflux.git
|
||||
cd mainflux
|
||||
git remote add upstream https://github.com/mainflux/mainflux.git
|
||||
```
|
||||
|
||||
If you cloned a while ago, get the latest changes from upstream:
|
||||
|
||||
```
|
||||
git checkout master
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
Create a new topic branch from `master` using the naming convention `MF-[issue-number]`
|
||||
to help us keep track of your contribution scope:
|
||||
|
||||
```
|
||||
git checkout -b MF-[issue-number]
|
||||
```
|
||||
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of working before making them public.
|
||||
|
||||
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`.
|
||||
|
||||
Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```
|
||||
git pull --rebase upstream master
|
||||
```
|
||||
|
||||
Push your topic branch up to your fork:
|
||||
|
||||
```
|
||||
git push origin MF-[issue-number]
|
||||
```
|
||||
|
||||
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
and detailed description.
|
||||
@@ -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:
|
||||
@@ -0,0 +1,4 @@
|
||||
build
|
||||
*.pb.go
|
||||
|
||||
site/
|
||||
Generated
+434
@@ -0,0 +1,434 @@
|
||||
# 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 = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
|
||||
version = "v9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
packages = ["."]
|
||||
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/containerd/continuity"
|
||||
packages = ["pathdriver"]
|
||||
revision = "c6cef34830231743494fe2969284df7b82cc0ad0"
|
||||
|
||||
[[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 = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
|
||||
version = "v0.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
packages = ["."]
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
"endpoint",
|
||||
"log",
|
||||
"metrics",
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"transport/grpc",
|
||||
"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",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/logfmt"
|
||||
packages = ["."]
|
||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid"
|
||||
]
|
||||
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
|
||||
|
||||
[[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 = "062418ea1c2181f52dc0f954f6204370519a868b"
|
||||
version = "v1.5.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/ory/dockertest"
|
||||
packages = [
|
||||
"docker",
|
||||
"docker/opts",
|
||||
"docker/pkg/archive",
|
||||
"docker/pkg/fileutils",
|
||||
"docker/pkg/homedir",
|
||||
"docker/pkg/idtools",
|
||||
"docker/pkg/ioutils",
|
||||
"docker/pkg/jsonmessage",
|
||||
"docker/pkg/longpath",
|
||||
"docker/pkg/mount",
|
||||
"docker/pkg/pools",
|
||||
"docker/pkg/stdcopy",
|
||||
"docker/pkg/system",
|
||||
"docker/pkg/term",
|
||||
"docker/pkg/term/windows",
|
||||
"docker/types",
|
||||
"docker/types/blkiodev",
|
||||
"docker/types/container",
|
||||
"docker/types/filters",
|
||||
"docker/types/mount",
|
||||
"docker/types/network",
|
||||
"docker/types/registry",
|
||||
"docker/types/strslice",
|
||||
"docker/types/versions"
|
||||
]
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
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 = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
packages = [
|
||||
".",
|
||||
"sqlparse"
|
||||
]
|
||||
revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[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 = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/timeseries",
|
||||
"trace"
|
||||
]
|
||||
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "7dfd1290c7917b7ba22824b9d24954ab3002fe24"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"channelz",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclb/grpc_lb_v1/messages",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "41344da2231b913fa3d983840a57a6b1b7b631a1"
|
||||
version = "v1.12.0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/gorp.v1"
|
||||
packages = ["."]
|
||||
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "cce4c6cd08b6213615afc135d8e7d4f255d4ee32b42062e1f3208de246366418"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
[[constraint]]
|
||||
name = "github.com/asaskevich/govalidator"
|
||||
version = "9.0.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
version = "0.6.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-zoo/bone"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jinzhu/gorm"
|
||||
version = "1.9.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
version = "1.4.0"
|
||||
|
||||
[[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 = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.1.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
@@ -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-2018 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
@@ -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"
|
||||
@@ -0,0 +1,56 @@
|
||||
BUILD_DIR = build
|
||||
SERVICES = users clients http normalizer ws coap
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOOS ?= linux
|
||||
|
||||
define compile_service
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
docker build --build-arg SVC_NAME=$(subst docker_,,$(1)) --tag=mainflux/$(subst docker_,,$(1)) -f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers latest release
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
proto:
|
||||
protoc --go_out=plugins=grpc:. *.proto
|
||||
|
||||
$(SERVICES): proto
|
||||
$(call compile_service,$(@))
|
||||
|
||||
$(DOCKERS):
|
||||
$(call make_docker,$(@))
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
|
||||
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile mqtt
|
||||
|
||||
latest: dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc; \
|
||||
done
|
||||
docker push mainflux/dashflux
|
||||
docker push mainflux/mqtt
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker push mainflux/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version)
|
||||
docker push mainflux/dashflux:$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt:$(version)
|
||||
docker push mainflux/mqtt:$(version)
|
||||
@@ -0,0 +1,67 @@
|
||||
# Mainflux
|
||||
|
||||
[![build][ci-badge]][ci-url]
|
||||
[![go report card][grc-badge]][grc-url]
|
||||
[![coverage][cov-badge]][cov-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
|
||||
docker-compose -f docker/docker-compose.yml 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/mainflux/blob/master/docs/img/gopherBanner.jpg
|
||||
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
|
||||
[ci-url]: https://semaphoreci.com/mainflux/mainflux
|
||||
[docs]: http://mainflux.readthedocs.io
|
||||
[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
|
||||
[cov-badge]: https://codecov.io/gh/mainflux/mainflux/branch/master/graph/badge.svg
|
||||
[cov-url]: https://codecov.io/gh/mainflux/mainflux
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
-508
@@ -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>"
|
||||
@@ -0,0 +1,13 @@
|
||||
package mainflux
|
||||
|
||||
// Response contains HTTP response specific methods.
|
||||
type Response interface {
|
||||
// Code returns HTTP response code.
|
||||
Code() int
|
||||
|
||||
// Headers returns map of HTTP headers with their values.
|
||||
Headers() map[string]string
|
||||
|
||||
// Empty indicates if HTTP response has content.
|
||||
Empty() bool
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
-722
@@ -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
File diff suppressed because it is too large
Load Diff
-1585
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
# Clients
|
||||
|
||||
Clients service provides an HTTP API for managing platform resources: devices,
|
||||
applications and channels. Through this API clients are able to do the following
|
||||
actions:
|
||||
|
||||
- provision new clients (i.e. devices & applications)
|
||||
- create new channels
|
||||
- "connect" clients into the channels
|
||||
|
||||
For in-depth explanation of the aforementioned scenarios, as well as thorough
|
||||
understanding of Mainflux, please check out the [official documentation][doc].
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|------------------------|------------------------------------------|----------------|
|
||||
| MF_CLIENTS_DB_HOST | Database host address | localhost |
|
||||
| MF_CLIENTS_DB_PORT | Database host port | 5432 |
|
||||
| MF_CLIENTS_DB_USER | Database user | mainflux |
|
||||
| MF_CLIENTS_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_CLIENTS_DB | Name of the database used by the service | clients |
|
||||
| MF_CLIENTS_HTTP_PORT | Clients service HTTP port | 8180 |
|
||||
| MF_CLIENTS_GRPC_PORT | Clients service gRPC port | 8181 |
|
||||
| MF_USERS_URL | Users service URL | localhost:8181 |
|
||||
| MF_CLIENTS_SECRET | String used for signing tokens | clients |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
clients:
|
||||
image: mainflux/clients:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MF_CLIENTS_DB_HOST: [Database host address]
|
||||
MF_CLIENTS_DB_PORT: [Database host port]
|
||||
MF_CLIENTS_DB_USER: [Database user]
|
||||
MF_CLIENTS_DB_PASS: [Database password]
|
||||
MF_CLIENTS_DB: [Name of the database used by the service]
|
||||
MF_CLIENTS_HTTP_PORT: [Service HTTP port]
|
||||
MF_CLIENTS_GRPC_PORT: [Service gRPC port]
|
||||
MF_USERS_URL: [Users service URL]
|
||||
MF_CLIENTS_SECRET: [String used for signing tokens]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the clients
|
||||
make clients
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_CLIENTS_DB_HOST=[Database host address] MF_CLIENTS_DB_PORT=[Database host port] MF_CLIENTS_DB_USER=[Database user] MF_CLIENTS_DB_PASS=[Database password] MF_CLIENTS_DB=[Name of the database used by the service] MF_CLIENTS_HTTP_PORT=[Service HTTP port] MF_CLIENTS_GRPC_PORT=[Service gRPC port] MF_USERS_URL=[Users service URL] MF_CLIENTS_SECRET=[String used for signing tokens] $GOBIN/mainflux-clients
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -0,0 +1,49 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.ClientsServiceClient = (*grpcClient)(nil)
|
||||
|
||||
type grpcClient struct {
|
||||
canAccess endpoint.Endpoint
|
||||
}
|
||||
|
||||
// NewClient returns new gRPC client instance.
|
||||
func NewClient(conn *grpc.ClientConn) mainflux.ClientsServiceClient {
|
||||
endpoint := kitgrpc.NewClient(
|
||||
conn,
|
||||
"mainflux.ClientsService",
|
||||
"CanAccess",
|
||||
encodeCanAccessRequest,
|
||||
decodeCanAccessResponse,
|
||||
mainflux.Identity{},
|
||||
).Endpoint()
|
||||
|
||||
return &grpcClient{endpoint}
|
||||
}
|
||||
|
||||
func (client grpcClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, _ ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
res, err := client.canAccess(ctx, accessReq{req.GetToken(), req.GetChanID()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ar := res.(accessRes)
|
||||
return &mainflux.Identity{Value: ar.id}, ar.err
|
||||
}
|
||||
|
||||
func encodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(accessReq)
|
||||
return &mainflux.AccessReq{Token: req.clientKey, ChanID: req.chanID}, nil
|
||||
}
|
||||
|
||||
func decodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(*mainflux.Identity)
|
||||
return accessRes{res.GetValue(), nil}, nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package grpc contains implementation of clients service gRPC API.
|
||||
package grpc
|
||||
@@ -0,0 +1,22 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func canAccessEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(accessReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.CanAccess(req.clientKey, req.chanID)
|
||||
if err != nil {
|
||||
return accessRes{"", err}, err
|
||||
}
|
||||
return accessRes{id, nil}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8080
|
||||
token = "token"
|
||||
wrong = "wrong"
|
||||
email = "john.doe@email.com"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = clients.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc clients.Service, port int) {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
|
||||
go server.Serve(listener)
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
startGRPCServer(svc, port)
|
||||
|
||||
connectedClientID, _ := svc.AddClient(token, client)
|
||||
connectedClient, _ := svc.ViewClient(token, connectedClientID)
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client, _ := svc.ViewClient(token, clientID)
|
||||
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
svc.Connect(token, chanID, connectedClientID)
|
||||
|
||||
usersAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
cli := grpcapi.NewClient(conn)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
cases := map[string]struct {
|
||||
clientKey string
|
||||
chanID string
|
||||
id string
|
||||
code codes.Code
|
||||
}{
|
||||
"check if connected client can access existing channel": {connectedClient.Key, chanID, connectedClientID, codes.OK},
|
||||
"check if unconnected client can access existing channel": {client.Key, chanID, "", codes.PermissionDenied},
|
||||
"check if wrong client can access existing channel": {wrong, chanID, "", codes.PermissionDenied},
|
||||
"check if connected client can access non-existent channel": {connectedClient.Key, "1", "", codes.InvalidArgument},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
id, err := cli.CanAccess(ctx, &mainflux.AccessReq{tc.clientKey, tc.chanID})
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "OK expected to be true")
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", desc, tc.id, id.GetValue()))
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
type accessReq struct {
|
||||
clientKey string
|
||||
chanID string
|
||||
}
|
||||
|
||||
func (req accessReq) validate() error {
|
||||
if !govalidator.IsUUID(req.chanID) || req.clientKey == "" {
|
||||
return clients.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package grpc
|
||||
|
||||
type accessRes struct {
|
||||
id string
|
||||
err error
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var _ mainflux.ClientsServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
handler kitgrpc.Handler
|
||||
}
|
||||
|
||||
// NewServer returns new ClientsServiceServer instance.
|
||||
func NewServer(svc clients.Service) mainflux.ClientsServiceServer {
|
||||
handler := kitgrpc.NewServer(
|
||||
canAccessEndpoint(svc),
|
||||
decodeCanAccessRequest,
|
||||
encodeCanAccessResponse,
|
||||
)
|
||||
return &grpcServer{handler}
|
||||
}
|
||||
|
||||
func (s *grpcServer) CanAccess(ctx context.Context, req *mainflux.AccessReq) (*mainflux.Identity, error) {
|
||||
_, res, err := s.handler.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return nil, encodeError(err)
|
||||
}
|
||||
return res.(*mainflux.Identity), nil
|
||||
}
|
||||
|
||||
func decodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
|
||||
req := grpcReq.(*mainflux.AccessReq)
|
||||
return accessReq{req.GetToken(), req.GetChanID()}, nil
|
||||
}
|
||||
|
||||
func encodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
|
||||
res := grpcRes.(accessRes)
|
||||
return &mainflux.Identity{Value: res.id}, encodeError(res.err)
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch err {
|
||||
case clients.ErrMalformedEntity:
|
||||
return status.Error(codes.InvalidArgument, "received invalid can access request")
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
return status.Error(codes.PermissionDenied, "missing or invalid credentials provided")
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package http contains implementation of clients service HTTP API.
|
||||
package http
|
||||
@@ -0,0 +1,217 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
func addClientEndpoint(svc clients.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 clients.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 clients.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 clients.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, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listClientsRes{clients}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeClientEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
err := req.validate()
|
||||
if err == clients.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 clients.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 clients.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 clients.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 clients.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, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listChannelsRes{channels}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelEndpoint(svc clients.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
if err == clients.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 clients.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 clients.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,610 @@
|
||||
package http_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
httpapi "github.com/mainflux/mainflux/clients/api/http"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
invalidEmail = "userexample.com"
|
||||
email = "user@example.com"
|
||||
token = "token"
|
||||
invalidToken = "invalid_token"
|
||||
wrongID = "123e4567-e89b-12d3-a456-000000000042"
|
||||
id = "123e4567-e89b-12d3-a456-000000000001"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
|
||||
channel = clients.Channel{Name: "test"}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
token string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", tr.token)
|
||||
}
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func newServer(svc clients.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
jsonData, _ := json.Marshal(data)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(clients.Client{
|
||||
Type: "foo",
|
||||
Name: "invalid_client",
|
||||
Payload: "some_payload",
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"add valid client", data, contentType, token, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
|
||||
{"add client with invalid data", invalidData, contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with invalid auth token", data, contentType, invalidToken, http.StatusForbidden, ""},
|
||||
{"add client with invalid request format", "}", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with empty JSON request", "{}", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with empty request", "", contentType, token, http.StatusBadRequest, ""},
|
||||
{"add client with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/clients", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := toJSON(client)
|
||||
invalidData := toJSON(clients.Client{
|
||||
Type: "foo",
|
||||
Name: client.Name,
|
||||
Payload: client.Payload,
|
||||
})
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing client", data, id, contentType, token, http.StatusOK},
|
||||
{"update non-existent client", data, wrongID, contentType, token, http.StatusNotFound},
|
||||
{"update client with invalid id", data, "1", contentType, token, http.StatusNotFound},
|
||||
{"update client with invalid data", invalidData, id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with invalid user token", data, id, contentType, invalidToken, http.StatusForbidden},
|
||||
{"update client with invalid data format", "{", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with empty JSON request", "{}", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with empty request", "", id, contentType, token, http.StatusBadRequest},
|
||||
{"update client with missing content type", data, id, "", token, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
fmt.Println(req.url)
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data := toJSON(client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing client", id, token, http.StatusOK, data},
|
||||
{"view non-existent client", wrongID, token, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid id", "1", token, http.StatusNotFound, ""},
|
||||
{"view client by passing invalid token", id, invalidToken, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data := strings.Trim(string(body), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
noClientsToken := "no_clients_token"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
noClientsToken: "no_clients_user@example.com",
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
data := []clients.Client{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.AddClient(token, client)
|
||||
client.ID = id
|
||||
client.Key = id
|
||||
data = append(data, client)
|
||||
}
|
||||
clientURL := fmt.Sprintf("%s/clients", ts.URL)
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []clients.Client
|
||||
}{
|
||||
{"get a list of clients", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), data[0:5]},
|
||||
{"get a list of clients with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
|
||||
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
|
||||
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
|
||||
{"get a list of clients with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
|
||||
{"get a list of clients with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), data[0:5]},
|
||||
{"get a list of clients with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), data[1:11]},
|
||||
{"get a list of clients with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), data[0:5]},
|
||||
{"get a list of clients with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
|
||||
{"get a list of clients with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), data[0:10]},
|
||||
{"get a list of clients with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
|
||||
{"get a list of clients with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var data map[string][]clients.Client
|
||||
json.NewDecoder(res.Body).Decode(&data)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
id, _ := svc.AddClient(token, client)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"delete existing client", id, token, http.StatusNoContent},
|
||||
{"delete non-existent client", wrongID, token, http.StatusNoContent},
|
||||
{"delete client with invalid id", "1", token, http.StatusNoContent},
|
||||
{"delete client with invalid token", id, invalidToken, http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
location string
|
||||
}{
|
||||
{"create new channel", data, contentType, token, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
|
||||
{"create new channel with invalid token", data, contentType, invalidToken, http.StatusForbidden, ""},
|
||||
{"create new channel with invalid data format", "{", contentType, token, http.StatusBadRequest, ""},
|
||||
{"create new channel with empty JSON request", "{}", contentType, token, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
|
||||
{"create new channel with empty request", "", contentType, token, http.StatusBadRequest, ""},
|
||||
{"create new channel with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPost,
|
||||
url: fmt.Sprintf("%s/channels", ts.URL),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
location := res.Header.Get("Location")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
updateData := toJSON(map[string]string{
|
||||
"name": "updated_channel",
|
||||
})
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
id string
|
||||
contentType string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"update existing channel", updateData, id, contentType, token, http.StatusOK},
|
||||
{"update non-existing channel", updateData, wrongID, contentType, token, http.StatusNotFound},
|
||||
{"update channel with invalid token", updateData, id, contentType, invalidToken, http.StatusForbidden},
|
||||
{"update channel with invalid id", updateData, "1", contentType, token, http.StatusNotFound},
|
||||
{"update channel with invalid data format", "}", id, contentType, token, http.StatusBadRequest},
|
||||
{"update channel with empty JSON object", "{}", id, contentType, token, http.StatusOK},
|
||||
{"update channel with empty request", "", id, contentType, token, http.StatusBadRequest},
|
||||
{"update channel with missing content type", updateData, id, "", token, http.StatusUnsupportedMediaType},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
contentType: tc.contentType,
|
||||
token: tc.auth,
|
||||
body: strings.NewReader(tc.req),
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
data := toJSON(channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
res string
|
||||
}{
|
||||
{"view existing channel", id, token, http.StatusOK, data},
|
||||
{"view non-existent channel", wrongID, token, http.StatusNotFound, ""},
|
||||
{"view channel with invalid id", "1", token, http.StatusNotFound, ""},
|
||||
{"view channel with invalid token", id, invalidToken, http.StatusForbidden, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
body := strings.Trim(string(data), "\n")
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
channels := []clients.Channel{}
|
||||
for i := 0; i < 101; i++ {
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
channelURL := fmt.Sprintf("%s/channels", ts.URL)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
status int
|
||||
url string
|
||||
res []clients.Channel
|
||||
}{
|
||||
{"get a list of channels", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
|
||||
{"get a list of channels with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
|
||||
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
|
||||
{"get a list of channels with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
|
||||
{"get a list of channels with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
|
||||
{"get a list of channels with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
|
||||
{"get a list of channels with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", channelURL, 0, 5), channels[0:5]},
|
||||
{"get a list of channels with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
|
||||
{"get a list of channels with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
|
||||
{"get a list of channels with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
|
||||
{"get a list of channels with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
|
||||
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
|
||||
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"), nil},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
var body map[string][]clients.Channel
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
id, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = id
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"remove existing channel", channel.ID, token, http.StatusNoContent},
|
||||
{"remove non-existent channel", channel.ID, token, http.StatusNoContent},
|
||||
{"remove channel with invalid id", wrongID, token, http.StatusNoContent},
|
||||
{"remove channel with invalid token", channel.ID, "invalidToken", http.StatusForbidden},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: client,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
otherToken := "other_token"
|
||||
otherEmail := "other_user@example.com"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
otherToken: otherEmail,
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
otherClientID, _ := svc.AddClient(otherToken, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherToken, channel)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"connect existing client to existing channel", chanID, clientID, token, http.StatusOK},
|
||||
{"connect existing client to non-existent channel", wrongID, clientID, token, http.StatusNotFound},
|
||||
{"connect client with invalid id to channel", chanID, "1", token, http.StatusNotFound},
|
||||
{"connect client to channel with invalid id", "1", clientID, token, http.StatusNotFound},
|
||||
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
|
||||
{"connect client from owner to channel of other user", otherChanID, clientID, token, http.StatusNotFound},
|
||||
{"connect client from other user to owner's channel", chanID, otherClientID, token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnnect(t *testing.T) {
|
||||
otherToken := "other_token"
|
||||
otherEmail := "other_user@example.com"
|
||||
svc := newService(map[string]string{
|
||||
token: email,
|
||||
otherToken: otherEmail,
|
||||
})
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
cli := ts.Client()
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
svc.Connect(token, chanID, clientID)
|
||||
otherClientID, _ := svc.AddClient(otherToken, client)
|
||||
otherChanID, _ := svc.CreateChannel(otherToken, channel)
|
||||
svc.Connect(otherToken, otherChanID, otherClientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chanID string
|
||||
clientID string
|
||||
auth string
|
||||
status int
|
||||
}{
|
||||
{"disconnect connected client from channel", chanID, clientID, token, http.StatusNoContent},
|
||||
{"disconnect non-connected client from channel", chanID, clientID, token, http.StatusNotFound},
|
||||
{"disconnect non-existent client from channel", chanID, "1", token, http.StatusNotFound},
|
||||
{"disconnect client from non-existent channel", "1", clientID, token, http.StatusNotFound},
|
||||
{"disconnect client from channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
|
||||
{"disconnect owner's client from someone elses channel", otherChanID, clientID, token, http.StatusNotFound},
|
||||
{"disconnect other's client from owner's channel", chanID, otherClientID, token, http.StatusNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: cli,
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type identityReq struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type addClientReq struct {
|
||||
key string
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req addClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
}
|
||||
|
||||
type updateClientReq struct {
|
||||
key string
|
||||
id string
|
||||
client clients.Client
|
||||
}
|
||||
|
||||
func (req updateClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
}
|
||||
|
||||
type createChannelReq struct {
|
||||
key string
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelReq struct {
|
||||
key string
|
||||
id string
|
||||
channel clients.Channel
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewResourceReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req viewResourceReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listResourcesReq struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
}
|
||||
|
||||
func (req *listResourcesReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
return clients.ErrMalformedEntity
|
||||
}
|
||||
|
||||
type connectionReq struct {
|
||||
key string
|
||||
chanID string
|
||||
clientID string
|
||||
}
|
||||
|
||||
func (req connectionReq) validate() error {
|
||||
if req.key == "" {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanID) || !govalidator.IsUUID(req.clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const wrong string = "?"
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app"}
|
||||
channel = clients.Channel{}
|
||||
)
|
||||
|
||||
func TestIdentityReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"non-empty token": {uuid.NewV4().String(), nil},
|
||||
"empty token": {"", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := identityReq{tc.key}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client addition request": {client, key, nil},
|
||||
"missing token": {client, "", clients.ErrUnauthorizedAccess},
|
||||
"wrong client type": {clients.Client{Type: wrong}, key, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := addClientReq{
|
||||
key: tc.key,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client update request": {client, id, key, nil},
|
||||
"non-uuid client ID": {client, wrong, key, clients.ErrNotFound},
|
||||
"missing token": {client, id, "", clients.ErrUnauthorizedAccess},
|
||||
"wrong client type": {clients.Client{Type: "invalid"}, id, key, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := updateClientReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel creation request": {channel, key, nil},
|
||||
"missing token": {channel, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := createChannelReq{
|
||||
key: tc.key,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel update request": {channel, id, key, nil},
|
||||
"non-uuid channel ID": {channel, wrong, key, clients.ErrNotFound},
|
||||
"missing token": {channel, id, "", clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := updateChannelReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewResourceReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid resource viewing request": {id, key, nil},
|
||||
"missing token": {id, "", clients.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := viewResourceReq{tc.key, tc.id}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListResourcesReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
value := 10
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
err error
|
||||
}{
|
||||
"valid listing request": {key, value, value, nil},
|
||||
"missing token": {"", value, value, clients.ErrUnauthorizedAccess},
|
||||
"negative offset": {key, -value, value, clients.ErrMalformedEntity},
|
||||
"zero limit": {key, value, 0, clients.ErrMalformedEntity},
|
||||
"negative limit": {key, value, -value, clients.ErrMalformedEntity},
|
||||
"too big limit": {key, value, 20 * value, clients.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := listResourcesReq{
|
||||
key: tc.key,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*identityRes)(nil)
|
||||
_ mainflux.Response = (*removeRes)(nil)
|
||||
_ mainflux.Response = (*clientRes)(nil)
|
||||
_ mainflux.Response = (*viewClientRes)(nil)
|
||||
_ mainflux.Response = (*listClientsRes)(nil)
|
||||
_ mainflux.Response = (*channelRes)(nil)
|
||||
_ mainflux.Response = (*viewChannelRes)(nil)
|
||||
_ mainflux.Response = (*listChannelsRes)(nil)
|
||||
_ mainflux.Response = (*connectionRes)(nil)
|
||||
_ mainflux.Response = (*disconnectionRes)(nil)
|
||||
)
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (res identityRes) Headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-client-id": res.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (res identityRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res identityRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type clientRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res clientRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res clientRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/clients/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res clientRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewClientRes struct {
|
||||
clients.Client
|
||||
}
|
||||
|
||||
func (res viewClientRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewClientRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewClientRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listClientsRes struct {
|
||||
Clients []clients.Client `json:"clients"`
|
||||
}
|
||||
|
||||
func (res listClientsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listClientsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listClientsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res channelRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/channels/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
clients.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listChannelsRes struct {
|
||||
Channels []clients.Channel `json:"channels"`
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listChannelsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type connectionRes struct{}
|
||||
|
||||
func (res connectionRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res connectionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectionRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectionRes struct{}
|
||||
|
||||
func (res disconnectionRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectionRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc clients.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
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.GetFunc("/version", mainflux.Version("clients"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client clients.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := addClientReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
client: client,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var client clients.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel clients.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := createChannelReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
channel: channel,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
var channel clients.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
offset := 0
|
||||
limit := 10
|
||||
|
||||
off, lmt := q["offset"], q["limit"]
|
||||
|
||||
if len(off) > 1 || len(lmt) > 1 {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(off) == 1 {
|
||||
offset, err = strconv.Atoi(off[0])
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
}
|
||||
|
||||
if len(lmt) == 1 {
|
||||
limit, err = strconv.Atoi(lmt[0])
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
}
|
||||
req := listResourcesReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
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.(mainflux.Response); 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 clients.ErrMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case clients.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case clients.ErrNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case clients.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errInvalidQueryParams:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case io.ErrUnexpectedEOF:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case io.EOF:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ clients.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc clients.Service, logger log.Logger) clients.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddClient(key string, client clients.Client) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client clients.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_client for key %s and client %s took %s to complete", key, client.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client clients.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListClients(key string, offset, limit int) (clients []clients.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_clients for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListClients(key, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_client for key %s and client %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel clients.Channel) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method create_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel clients.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_channel for key %s and channel %s took %s to complete", key, channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel clients.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannels(key string, offset, limit int) (channels []clients.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_channels for key %s took %s to complete", key, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChannels(key, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_channel for key %s and channel %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Connect(key, chanID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method connect for key %s, channel %s, client %s took %s to complete", key, chanID, clientID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Connect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Disconnect(key, chanID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method disconnect for key %s, channel %s, client %s took %s to complete", key, chanID, clientID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Disconnect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method can_access for key %s, channel %s and publisher %s took %s to complete", key, id, pub, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CanAccess(key, id)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ clients.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc clients.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc clients.Service, counter metrics.Counter, latency metrics.Histogram) clients.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddClient(key string, client clients.Client) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_client").Add(1)
|
||||
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateClient(key string, client clients.Client) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_client").Add(1)
|
||||
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewClient(key string, id string) (clients.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_client").Add(1)
|
||||
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]clients.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_clients").Add(1)
|
||||
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListClients(key, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_client").Add(1)
|
||||
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateChannel(key string, channel clients.Channel) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_channel").Add(1)
|
||||
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateChannel(key string, channel clients.Channel) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_channel").Add(1)
|
||||
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewChannel(key string, id string) (clients.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_channel").Add(1)
|
||||
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]clients.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_channels").Add(1)
|
||||
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChannels(key, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_channel").Add(1)
|
||||
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Connect(key, chanID, clientID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "connect").Add(1)
|
||||
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Connect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Disconnect(key, chanID, clientID string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "disconnect").Add(1)
|
||||
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Disconnect(key, chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "can_access").Add(1)
|
||||
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CanAccess(key, id)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Package bcrypt provides a hasher implementation utilising bcrypt.
|
||||
package bcrypt
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const cost int = 10
|
||||
|
||||
var _ clients.Hasher = (*bcryptHasher)(nil)
|
||||
|
||||
type bcryptHasher struct{}
|
||||
|
||||
// New instantiates a bcrypt-based hasher implementation.
|
||||
func New() clients.Hasher {
|
||||
return &bcryptHasher{}
|
||||
}
|
||||
|
||||
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func (bh *bcryptHasher) Compare(plain, hashed string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package clients
|
||||
|
||||
// Channel represents a Mainflux "communication group". This group contains the
|
||||
// clients that can exchange messages between eachother.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"-"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Clients []Client `json:"connected,omitempty"`
|
||||
}
|
||||
|
||||
// ChannelRepository specifies a channel persistence API.
|
||||
type ChannelRepository interface {
|
||||
// Save persists the channel. Successful operation is indicated by unique
|
||||
// identifier accompanied by nil error response. A non-nil error is
|
||||
// returned to indicate operation failure.
|
||||
Save(Channel) (string, error)
|
||||
|
||||
// Update performs an update to the existing channel. A non-nil error is
|
||||
// returned to indicate operation failure.
|
||||
Update(Channel) error
|
||||
|
||||
// One retrieves the channel having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
One(string, string) (Channel, error)
|
||||
|
||||
// All retrieves the subset of channels owned by the specified user.
|
||||
All(string, int, int) []Channel
|
||||
|
||||
// Remove removes the channel having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// HasClient determines whether the client with the provided identifier, is
|
||||
// "connected" to the specified channel.
|
||||
HasClient(string, string) bool
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package clients
|
||||
|
||||
import "strings"
|
||||
|
||||
// Client represents a Mainflux client. Each client is owned by one user, and
|
||||
// it is assigned with the unique identifier and (temporary) access key.
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
var clientTypes = map[string]bool{
|
||||
"app": true,
|
||||
"device": true,
|
||||
}
|
||||
|
||||
// Validate returns an error if client representation is invalid.
|
||||
func (c *Client) Validate() error {
|
||||
if c.Type = strings.ToLower(c.Type); !clientTypes[c.Type] {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClientRepository specifies a client persistence API.
|
||||
type ClientRepository interface {
|
||||
// ID generates new resource identifier.
|
||||
ID() string
|
||||
|
||||
// Save persists the client. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(Client) error
|
||||
|
||||
// Update performs an update to the existing client. A non-nil error is
|
||||
// returned to indicate operation failure.
|
||||
Update(Client) error
|
||||
|
||||
// One retrieves the client having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
One(string, string) (Client, error)
|
||||
|
||||
// All retrieves the subset of clients owned by the specified user.
|
||||
All(string, int, int) []Client
|
||||
|
||||
// Remove removes the client having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package clients contains the domain concept definitions needed to support
|
||||
// Mainflux clients service functionality.
|
||||
package clients
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package jwt provides a JWT identity provider.
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
const (
|
||||
issuer string = "mainflux"
|
||||
duration time.Duration = 10 * time.Hour
|
||||
)
|
||||
|
||||
var _ clients.IdentityProvider = (*jwtIdentityProvider)(nil)
|
||||
|
||||
type jwtIdentityProvider struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// New instantiates a JWT identity provider.
|
||||
func New(secret string) clients.IdentityProvider {
|
||||
return &jwtIdentityProvider{secret}
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
|
||||
now := time.Now().UTC()
|
||||
exp := now.Add(duration)
|
||||
|
||||
claims := jwt.StandardClaims{
|
||||
Subject: id,
|
||||
Issuer: issuer,
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: exp.Unix(),
|
||||
}
|
||||
|
||||
return idp.jwt(claims)
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) PermanentKey(id string) (string, error) {
|
||||
claims := jwt.StandardClaims{
|
||||
Subject: id,
|
||||
Issuer: issuer,
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
return idp.jwt(claims)
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(idp.secret))
|
||||
}
|
||||
|
||||
func (idp *jwtIdentityProvider) Identity(key string) (string, error) {
|
||||
token, err := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return []byte(idp.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims["sub"].(string), nil
|
||||
}
|
||||
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ clients.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
|
||||
const chanID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type channelRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
channels map[string]clients.Channel
|
||||
clients clients.ClientRepository
|
||||
}
|
||||
|
||||
// NewChannelRepository creates in-memory channel repository.
|
||||
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
|
||||
return &channelRepositoryMock{
|
||||
channels: make(map[string]clients.Channel),
|
||||
clients: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Save(channel clients.Channel) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter++
|
||||
channel.ID = fmt.Sprintf("%s%012d", chanID, crm.counter)
|
||||
|
||||
crm.channels[key(channel.Owner, channel.ID)] = channel
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Update(channel clients.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(channel.Owner, channel.ID)
|
||||
|
||||
if _, ok := crm.channels[dbKey]; !ok {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.channels[dbKey] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (clients.Channel, error) {
|
||||
if c, ok := crm.channels[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return clients.Channel{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []clients.Channel {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
channels := make([]clients.Channel, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return channels
|
||||
}
|
||||
|
||||
// Since IDs starts from 1, shift everything by one.
|
||||
first := fmt.Sprintf("%s%012d", chanID, offset+1)
|
||||
last := fmt.Sprintf("%s%012d", chanID, offset+limit+1)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
|
||||
channels = append(channels, v)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.channels, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Connect(owner, chanID, clientID string) error {
|
||||
channel, err := crm.One(owner, chanID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := crm.clients.One(owner, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channel.Clients = append(channel.Clients, client)
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Disconnect(owner, chanID, clientID string) error {
|
||||
channel, err := crm.One(owner, chanID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !crm.HasClient(chanID, clientID) {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
connected := make([]clients.Client, len(channel.Clients)-1)
|
||||
for _, client := range channel.Clients {
|
||||
if client.ID != clientID {
|
||||
connected = append(connected, client)
|
||||
}
|
||||
}
|
||||
|
||||
channel.Clients = connected
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
suffix := fmt.Sprintf("-%s", channel)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasSuffix(k, suffix) {
|
||||
for _, c := range v.Clients {
|
||||
if c.ID == client {
|
||||
return true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
|
||||
const cliID = "123e4567-e89b-12d3-a456-"
|
||||
|
||||
type clientRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
clients map[string]clients.Client
|
||||
}
|
||||
|
||||
// NewClientRepository creates in-memory client repository.
|
||||
func NewClientRepository() clients.ClientRepository {
|
||||
return &clientRepositoryMock{
|
||||
clients: make(map[string]clients.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) ID() string {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter++
|
||||
return fmt.Sprintf("%s%012d", cliID, crm.counter)
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Save(client clients.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.clients[key(client.Owner, client.ID)] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Update(client clients.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(client.Owner, client.ID)
|
||||
|
||||
if _, ok := crm.clients[dbKey]; !ok {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
crm.clients[dbKey] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
|
||||
if c, ok := crm.clients[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return clients.Client{}, clients.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []clients.Client {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
clients := make([]clients.Client, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return clients
|
||||
}
|
||||
|
||||
// Since IDs start from 1, shift everything by one.
|
||||
first := fmt.Sprintf("%s%012d", cliID, offset+1)
|
||||
last := fmt.Sprintf("%s%012d", cliID, offset+limit+1)
|
||||
|
||||
for k, v := range crm.clients {
|
||||
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
|
||||
clients = append(clients, v)
|
||||
}
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.clients, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
)
|
||||
|
||||
var (
|
||||
_ clients.Hasher = (*hasherMock)(nil)
|
||||
_ clients.IdentityProvider = (*identityProviderMock)(nil)
|
||||
)
|
||||
|
||||
type hasherMock struct{}
|
||||
|
||||
func (hm *hasherMock) Hash(pwd string) (string, error) {
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
func (hm *hasherMock) Compare(plain, hashed string) error {
|
||||
if plain != hashed {
|
||||
return clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type identityProviderMock struct{}
|
||||
|
||||
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
|
||||
if id == "" {
|
||||
return "", clients.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) PermanentKey(id string) (string, error) {
|
||||
return idp.TemporaryKey(id)
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) Identity(key string) (string, error) {
|
||||
return idp.TemporaryKey(key)
|
||||
}
|
||||
|
||||
// NewHasher creates "no-op" hasher for test purposes. This implementation will
|
||||
// return secrets without changing them.
|
||||
func NewHasher() clients.Hasher {
|
||||
return &hasherMock{}
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() clients.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceClient = (*usersServiceMock)(nil)
|
||||
|
||||
type usersServiceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.UsersServiceClient {
|
||||
return &usersServiceMock{users}
|
||||
}
|
||||
|
||||
func (svc usersServiceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.Identity, error) {
|
||||
if id, ok := svc.users[in.Value]; ok {
|
||||
return &mainflux.Identity{Value: id}, nil
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ clients.ChannelRepository = (*channelRepository)(nil)
|
||||
|
||||
const (
|
||||
errDuplicate = "unique_violation"
|
||||
errFK = "foreign_key_violation"
|
||||
)
|
||||
|
||||
type channelRepository struct {
|
||||
db *sql.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewChannelRepository instantiates a PostgreSQL implementation of channel
|
||||
// repository.
|
||||
func NewChannelRepository(db *sql.DB, log logger.Logger) clients.ChannelRepository {
|
||||
return &channelRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr channelRepository) Save(channel clients.Channel) (string, error) {
|
||||
channel.ID = uuid.NewV4().String()
|
||||
|
||||
q := `INSERT INTO channels (id, owner, name) VALUES ($1, $2, $3)`
|
||||
|
||||
_, err := cr.db.Exec(q, channel.ID, channel.Owner, channel.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Update(channel clients.Channel) error {
|
||||
q := `UPDATE channels SET name = $1 WHERE owner = $2 AND id = $3;`
|
||||
|
||||
res, err := cr.db.Exec(q, channel.Name, channel.Owner, channel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) One(owner, id string) (clients.Channel, error) {
|
||||
q := `SELECT name FROM channels WHERE id = $1 AND owner = $2`
|
||||
channel := clients.Channel{ID: id, Owner: owner}
|
||||
if err := cr.db.QueryRow(q, id, owner).Scan(&channel.Name); err != nil {
|
||||
empty := clients.Channel{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, clients.ErrNotFound
|
||||
}
|
||||
return empty, err
|
||||
}
|
||||
|
||||
qr := `SELECT id, type, name, key, payload FROM clients cli
|
||||
INNER JOIN connections conn
|
||||
ON cli.id = conn.client_id AND cli.owner = conn.client_owner
|
||||
WHERE conn.channel_id = $1 AND conn.channel_owner = $2`
|
||||
|
||||
rows, err := cr.db.Query(qr, id, owner)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return clients.Channel{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := clients.Client{Owner: owner}
|
||||
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return clients.Channel{}, err
|
||||
}
|
||||
channel.Clients = append(channel.Clients, c)
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) All(owner string, offset, limit int) []clients.Channel {
|
||||
q := `SELECT id, name FROM channels WHERE owner = $1 LIMIT $2 OFFSET $3`
|
||||
items := []clients.Channel{}
|
||||
|
||||
rows, err := cr.db.Query(q, owner, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve channels due to %s", err))
|
||||
return []clients.Channel{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := clients.Channel{Owner: owner}
|
||||
if err = rows.Scan(&c.ID, &c.Name); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channel due to %s", err))
|
||||
return []clients.Channel{}
|
||||
}
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (cr channelRepository) Remove(owner, id string) error {
|
||||
q := `DELETE FROM channels WHERE id = $1 AND owner = $2`
|
||||
cr.db.Exec(q, id, owner)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Connect(owner, chanID, clientID string) error {
|
||||
q := `INSERT INTO connections (channel_id, channel_owner, client_id, client_owner) VALUES ($1, $2, $3, $2)`
|
||||
|
||||
if _, err := cr.db.Exec(q, chanID, owner, clientID); err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
|
||||
if ok && errFK == pqErr.Code.Name() {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
// connect is idempotent
|
||||
if ok && errDuplicate == pqErr.Code.Name() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Disconnect(owner, chanID, clientID string) error {
|
||||
q := `DELETE FROM connections
|
||||
WHERE channel_id = $1 AND channel_owner = $2
|
||||
AND client_id = $3 AND client_owner = $2`
|
||||
|
||||
res, err := cr.db.Exec(q, chanID, owner, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) HasClient(chanID, clientID string) bool {
|
||||
q := "SELECT EXISTS (SELECT 1 FROM connections WHERE channel_id = $1 AND client_id = $2);"
|
||||
|
||||
exists := false
|
||||
if err := cr.db.QueryRow(q, chanID, clientID).Scan(&exists); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to check client existence due to %s", err))
|
||||
}
|
||||
return exists
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChannelSave(t *testing.T) {
|
||||
email := "channel-save@example.com"
|
||||
channel := clients.Channel{Owner: email}
|
||||
|
||||
channelRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
_, err := channelRepo.Save(channel)
|
||||
hasErr := err != nil
|
||||
assert.False(t, hasErr, fmt.Sprintf("create new channel: expected false got %t", hasErr))
|
||||
}
|
||||
|
||||
func TestChannelUpdate(t *testing.T) {
|
||||
email := "channel-update@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
c := clients.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
c.ID = id
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
err error
|
||||
}{
|
||||
"existing channel": {c, nil},
|
||||
"non-existing channel with existing user": {clients.Channel{ID: wrong, Owner: email}, clients.ErrNotFound},
|
||||
"non-existing channel with non-existing user": {clients.Channel{ID: wrong, Owner: wrong}, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Update(tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleChannelRetrieval(t *testing.T) {
|
||||
email := "channel-single-retrieval@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
c := clients.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, id, nil},
|
||||
"existing user, non-existing channel": {c.Owner, wrong, clients.ErrNotFound},
|
||||
"non-existing owner": {wrong, id, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := chanRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiChannelRetrieval(t *testing.T) {
|
||||
email := "channel-multi-retrieval@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := clients.Channel{Owner: email}
|
||||
chanRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
}{
|
||||
"existing owner": {email, 0, n, n},
|
||||
"non-existing owner": {wrong, 1, 6, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
size := len(chanRepo.All(tc.owner, tc.offset, tc.limit))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelRemoval(t *testing.T) {
|
||||
email := "channel-removal@example.com"
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) channel
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := chanRepo.Remove(email, chanID); err != nil {
|
||||
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := chanRepo.One(email, chanID); err != clients.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConnect(t *testing.T) {
|
||||
email := "channel-connect@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"existing user, channel and client", email, chanID, client.ID, nil},
|
||||
{"connected channel and client", email, chanID, client.ID, nil},
|
||||
{"with non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
|
||||
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
|
||||
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := chanRepo.Connect(tc.owner, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisconnect(t *testing.T) {
|
||||
email := "channel-disconnect@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanID, client.ID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"connected client", email, chanID, client.ID, nil},
|
||||
{"non-connected client", email, chanID, client.ID, clients.ErrNotFound},
|
||||
{"non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
|
||||
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
|
||||
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := chanRepo.Disconnect(tc.owner, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelAccessCheck(t *testing.T) {
|
||||
email := "channel-access-check@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db, testLog)
|
||||
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanID, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
chanID string
|
||||
clientID string
|
||||
hasAccess bool
|
||||
}{
|
||||
"client that has access": {chanID, client.ID, true},
|
||||
"client without access": {chanID, wrong, false},
|
||||
"check access to non-existing channel": {wrong, client.ID, false},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasAccess := chanRepo.HasClient(tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq" // required for DB access
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ clients.ClientRepository = (*clientRepository)(nil)
|
||||
|
||||
type clientRepository struct {
|
||||
db *sql.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewClientRepository instantiates a PostgreSQL implementation of client
|
||||
// repository.
|
||||
func NewClientRepository(db *sql.DB, log logger.Logger) clients.ClientRepository {
|
||||
return &clientRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr clientRepository) ID() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
func (cr clientRepository) Save(client clients.Client) error {
|
||||
q := `INSERT INTO clients (id, owner, type, name, key, payload) VALUES ($1, $2, $3, $4, $5, $6)`
|
||||
_, err := cr.db.Exec(q, client.ID, client.Owner, client.Type, client.Name, client.Key, client.Payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr clientRepository) Update(client clients.Client) error {
|
||||
q := `UPDATE clients SET name = $1, payload = $2 WHERE owner = $3 AND id = $4;`
|
||||
|
||||
res, err := cr.db.Exec(q, client.Name, client.Payload, client.Owner, client.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return clients.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr clientRepository) One(owner, id string) (clients.Client, error) {
|
||||
q := `SELECT name, type, key, payload FROM clients WHERE id = $1 AND owner = $2`
|
||||
client := clients.Client{ID: id, Owner: owner}
|
||||
err := cr.db.
|
||||
QueryRow(q, id, owner).
|
||||
Scan(&client.Name, &client.Type, &client.Key, &client.Payload)
|
||||
|
||||
if err != nil {
|
||||
empty := clients.Client{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, clients.ErrNotFound
|
||||
}
|
||||
return empty, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cr clientRepository) All(owner string, offset, limit int) []clients.Client {
|
||||
q := `SELECT id, name, type, key, payload FROM clients WHERE owner = $1 LIMIT $2 OFFSET $3`
|
||||
items := []clients.Client{}
|
||||
|
||||
rows, err := cr.db.Query(q, owner, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve clients due to %s", err))
|
||||
return []clients.Client{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := clients.Client{Owner: owner}
|
||||
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved client due to %s", err))
|
||||
return []clients.Client{}
|
||||
}
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (cr clientRepository) Remove(owner, id string) error {
|
||||
q := `DELETE FROM clients WHERE id = $1 AND owner = $2`
|
||||
cr.db.Exec(q, id, owner)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientSave(t *testing.T) {
|
||||
email := "client-save@example.com"
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
hasErr := clientRepo.Save(client) != nil
|
||||
assert.False(t, hasErr, fmt.Sprintf("create new client: expected false got %t\n", hasErr))
|
||||
}
|
||||
|
||||
func TestClientUpdate(t *testing.T) {
|
||||
email := "client-update@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
err error
|
||||
}{
|
||||
"existing client": {c, nil},
|
||||
"non-existing client with existing user": {clients.Client{ID: wrong, Owner: email}, clients.ErrNotFound},
|
||||
"non-existing client with non-existing user": {clients.Client{ID: wrong, Owner: wrong}, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := clientRepo.Update(tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleClientRetrieval(t *testing.T) {
|
||||
email := "client-single-retrieval@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, c.ID, nil},
|
||||
"existing user, non-existing client": {c.Owner, wrong, clients.ErrNotFound},
|
||||
"non-existing owner": {wrong, c.ID, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := clientRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiClientRetrieval(t *testing.T) {
|
||||
email := "client-multi-retrieval@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
}{
|
||||
"existing owner, retrieve all": {email, 0, n, n},
|
||||
"existing owner, retrieve subset": {email, 1, 6, 6},
|
||||
"non-existing owner": {wrong, 1, 6, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
n := len(clientRepo.All(tc.owner, tc.offset, tc.limit))
|
||||
assert.Equal(t, tc.size, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemoval(t *testing.T) {
|
||||
email := "client-removal@example.com"
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db, testLog)
|
||||
client := clients.Client{
|
||||
ID: clientRepo.ID(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) client
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := clientRepo.Remove(email, client.ID); err != nil {
|
||||
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := clientRepo.One(email, client.ID); err != clients.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,72 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance and applies any
|
||||
// unapplied database migrations. A non-nil error is returned to indicate
|
||||
// failure.
|
||||
func Connect(host, port, name, user, pass string) (*sql.DB, error) {
|
||||
t := "host=%s port=%s user=%s dbname=%s password=%s sslmode=disable"
|
||||
url := fmt.Sprintf(t, host, port, user, name, pass)
|
||||
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sql.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
&migrate.Migration{
|
||||
Id: "clients_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE clients (
|
||||
id CHAR(36),
|
||||
owner VARCHAR(254),
|
||||
type VARCHAR(10) NOT NULL,
|
||||
name TEXT,
|
||||
key TEXT,
|
||||
payload TEXT,
|
||||
PRIMARY KEY (id, owner)
|
||||
)`,
|
||||
`CREATE TABLE channels (
|
||||
id CHAR(36),
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
PRIMARY KEY (id, owner)
|
||||
)`,
|
||||
`CREATE TABLE connections (
|
||||
channel_id CHAR(36),
|
||||
channel_owner VARCHAR(254),
|
||||
client_id CHAR(36),
|
||||
client_owner VARCHAR(254),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (id, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (client_id, client_owner) REFERENCES clients (id, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, client_id, client_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE clients",
|
||||
"DROP TABLE channels",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package postgres_test contains tests for PostgreSQL repository
|
||||
// implementations.
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var (
|
||||
testLog = logger.New(os.Stdout)
|
||||
db *sql.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect("localhost", port, "test", "test", "test"); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package clients
|
||||
|
||||
// Hasher specifies an API for generating hashes of an arbitrary textual
|
||||
// content.
|
||||
type Hasher interface {
|
||||
// Hash generates the hashed string from plain-text.
|
||||
Hash(string) (string, error)
|
||||
|
||||
// Compare compares plain-text version to the hashed one. An error should
|
||||
// indicate failed comparison.
|
||||
Compare(string, string) error
|
||||
}
|
||||
|
||||
// IdentityProvider specifies an API for identity management via security
|
||||
// tokens.
|
||||
type IdentityProvider interface {
|
||||
// TemporaryKey generates the temporary access token.
|
||||
TemporaryKey(string) (string, error)
|
||||
|
||||
// PermanentKey generates the non-expiring access token.
|
||||
PermanentKey(string) (string, error)
|
||||
|
||||
// Identity extracts the entity identifier given its secret key.
|
||||
Identity(string) (string, error)
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrConflict indicates usage of the existing email during account
|
||||
// registration.
|
||||
ErrConflict = errors.New("email already taken")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid username or password).
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound = errors.New("non-existent entity")
|
||||
)
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// AddClient adds new client to the user identified by the provided key.
|
||||
AddClient(string, Client) (string, error)
|
||||
|
||||
// UpdateClient updates the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
UpdateClient(string, Client) error
|
||||
|
||||
// ViewClient retrieves data about the client identified with the provided
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewClient(string, string) (Client, error)
|
||||
|
||||
// ListClients retrieves data about subset of clients that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListClients(string, int, int) ([]Client, error)
|
||||
|
||||
// RemoveClient removes the client identified with the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveClient(string, string) error
|
||||
|
||||
// CreateChannel adds new channel to the user identified by the provided key.
|
||||
CreateChannel(string, Channel) (string, error)
|
||||
|
||||
// UpdateChannel updates the channel identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
UpdateChannel(string, Channel) error
|
||||
|
||||
// ViewChannel retrieves data about the channel identified by the provided
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewChannel(string, string) (Channel, error)
|
||||
|
||||
// ListChannels retrieves data about subset of channels that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListChannels(string, int, int) ([]Channel, error)
|
||||
|
||||
// RemoveChannel removes the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveChannel(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// CanAccess determines whether the channel can be accessed using the
|
||||
// provided key and returns client's id.
|
||||
CanAccess(string, string) (string, error)
|
||||
}
|
||||
|
||||
var _ Service = (*clientsService)(nil)
|
||||
|
||||
type clientsService struct {
|
||||
users mainflux.UsersServiceClient
|
||||
clients ClientRepository
|
||||
channels ChannelRepository
|
||||
hasher Hasher
|
||||
idp IdentityProvider
|
||||
}
|
||||
|
||||
// New instantiates the clients service implementation.
|
||||
func New(users mainflux.UsersServiceClient, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
|
||||
return &clientsService{
|
||||
users: users,
|
||||
clients: clients,
|
||||
channels: channels,
|
||||
hasher: hasher,
|
||||
idp: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *clientsService) AddClient(key string, client Client) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
client.ID = ms.clients.ID()
|
||||
client.Owner = res.GetValue()
|
||||
client.Key, _ = ms.idp.PermanentKey(client.ID)
|
||||
|
||||
return client.ID, ms.clients.Save(client)
|
||||
}
|
||||
|
||||
func (ms *clientsService) UpdateClient(key string, client Client) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
client.Owner = res.GetValue()
|
||||
|
||||
return ms.clients.Update(client)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ViewClient(key, id string) (Client, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return Client{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.One(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ListClients(key string, offset, limit int) ([]Client, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.All(res.GetValue(), offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *clientsService) RemoveClient(key, id string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.Remove(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) CreateChannel(key string, channel Channel) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = res.GetValue()
|
||||
return ms.channels.Save(channel)
|
||||
}
|
||||
|
||||
func (ms *clientsService) UpdateChannel(key string, channel Channel) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = res.GetValue()
|
||||
return ms.channels.Update(channel)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ViewChannel(key, id string) (Channel, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return Channel{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.One(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) ListChannels(key string, offset, limit int) ([]Channel, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.All(res.GetValue(), offset, limit), nil
|
||||
}
|
||||
|
||||
func (ms *clientsService) RemoveChannel(key, id string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Remove(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ms *clientsService) Connect(key, chanID, clientID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Connect(res.GetValue(), chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *clientsService) Disconnect(key, chanID, clientID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
|
||||
if err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Disconnect(res.GetValue(), chanID, clientID)
|
||||
}
|
||||
|
||||
func (ms *clientsService) CanAccess(key, channel string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !ms.channels.HasClient(channel, client) {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package clients_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
wrong = "wrong-value"
|
||||
email = "user@example.com"
|
||||
token = "token"
|
||||
)
|
||||
|
||||
var (
|
||||
client = clients.Client{Type: "app", Name: "test"}
|
||||
channel = clients.Channel{Name: "test", Clients: []clients.Client{}}
|
||||
)
|
||||
|
||||
func newService(tokens map[string]string) clients.Service {
|
||||
users := mocks.NewUsersService(tokens)
|
||||
clientsRepo := mocks.NewClientRepository()
|
||||
channelsRepo := mocks.NewChannelRepository(clientsRepo)
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"add new app": {clients.Client{Type: "app", Name: "a"}, token, nil},
|
||||
"add new device": {clients.Client{Type: "device", Name: "b"}, token, nil},
|
||||
"add client with wrong credentials": {clients.Client{Type: "app", Name: "d"}, wrong, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.AddClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
|
||||
cases := map[string]struct {
|
||||
client clients.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing client": {client, token, nil},
|
||||
"update client with wrong credentials": {client, wrong, clients.ErrUnauthorizedAccess},
|
||||
"update non-existing client": {clients.Client{ID: "2", Type: "app", Name: "d"}, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing client": {client.ID, token, nil},
|
||||
"view client with wrong credentials": {client.ID, wrong, clients.ErrUnauthorizedAccess},
|
||||
"view non-existing client": {wrong, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.AddClient(token, client)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list clients": {token, 0, 5, 5, nil},
|
||||
"list clients 5-10": {token, 5, 10, 5, nil},
|
||||
"list last client": {token, 9, 10, 1, nil},
|
||||
"list empty response": {token, 11, 10, 0, nil},
|
||||
"list offset < 0": {token, -1, 10, 0, nil},
|
||||
"list limit < 0": {token, 1, -10, 0, nil},
|
||||
"list limit = 0": {token, 1, 0, 0, nil},
|
||||
"list clients with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
cl, err := svc.ListClients(tc.key, tc.offset, tc.limit)
|
||||
size := len(cl)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove client with wrong credentials": {client.ID, "?", clients.ErrUnauthorizedAccess},
|
||||
"remove existing client": {client.ID, token, nil},
|
||||
"remove removed client": {client.ID, token, nil},
|
||||
"remove non-existing client": {"?", token, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"create channel": {clients.Channel{}, token, nil},
|
||||
"create channel with wrong credentials": {clients.Channel{}, wrong, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CreateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
channel clients.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing channel": {channel, token, nil},
|
||||
"update channel with wrong credentials": {channel, wrong, clients.ErrUnauthorizedAccess},
|
||||
"update non-existing channel": {clients.Channel{ID: "2", Name: "test"}, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing channel": {channel.ID, token, nil},
|
||||
"view channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
|
||||
"view non-existing channel": {wrong, token, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
n := 10
|
||||
for i := 0; i < n; i++ {
|
||||
svc.CreateChannel(token, channel)
|
||||
}
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
offset int
|
||||
limit int
|
||||
size int
|
||||
err error
|
||||
}{
|
||||
"list first 5 channels": {token, 0, 5, 5, nil},
|
||||
"list channels 5-10 channels": {token, 5, 10, 5, nil},
|
||||
"list last channel": {token, 6, 10, 4, nil},
|
||||
"list offset < 0": {token, -1, 10, 0, nil},
|
||||
"list limit < 0": {token, 1, -10, 0, nil},
|
||||
"list limit = 0": {token, 1, 0, 0, nil},
|
||||
"list channels with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
ch, err := svc.ListChannels(tc.key, tc.offset, tc.limit)
|
||||
size := len(ch)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
|
||||
"remove existing channel": {channel.ID, token, nil},
|
||||
"remove removed channel": {channel.ID, token, nil},
|
||||
"remove non-existing channel": {channel.ID, token, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
"connect client": {token, channel.ID, client.ID, nil},
|
||||
"connect client with wrong credentials": {wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
|
||||
"connect client to non-existing channel": {token, wrong, client.ID, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Connect(tc.key, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
svc.Connect(token, chanID, clientID)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
chanID string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{"disconnect connected client", token, channel.ID, client.ID, nil},
|
||||
{"disconnect disconnected client", token, channel.ID, client.ID, clients.ErrNotFound},
|
||||
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
|
||||
{"disconnect client from non-existing channel", token, wrong, client.ID, clients.ErrNotFound},
|
||||
{"disconnect non-existing client", token, channel.ID, wrong, clients.ErrNotFound},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Disconnect(tc.key, tc.chanID, tc.clientID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService(map[string]string{token: email})
|
||||
|
||||
clientID, _ := svc.AddClient(token, client)
|
||||
client.ID = clientID
|
||||
client.Key = clientID
|
||||
|
||||
channel.Clients = []clients.Client{client}
|
||||
chanID, _ := svc.CreateChannel(token, channel)
|
||||
channel.ID = chanID
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
channel string
|
||||
err error
|
||||
}{
|
||||
"allowed access": {client.Key, channel.ID, nil},
|
||||
"not-connected cannot access": {"", channel.ID, clients.ErrUnauthorizedAccess},
|
||||
"access non-existing channel": {client.Key, wrong, clients.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CanAccess(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux clients service
|
||||
description: HTTP API for managing platform devices, applications and channels.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/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.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
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/Limit"
|
||||
- $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.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
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.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
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/Limit"
|
||||
- $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.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
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"
|
||||
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
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 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
|
||||
@@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/clients"
|
||||
"github.com/mainflux/mainflux/clients/api"
|
||||
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
httpapi "github.com/mainflux/mainflux/clients/api/http"
|
||||
"github.com/mainflux/mainflux/clients/bcrypt"
|
||||
"github.com/mainflux/mainflux/clients/jwt"
|
||||
"github.com/mainflux/mainflux/clients/postgres"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
usersapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "clients"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defUsersURL = "localhost:8181"
|
||||
defSecret = "clients"
|
||||
envDBHost = "MF_CLIENTS_DB_HOST"
|
||||
envDBPort = "MF_CLIENTS_DB_PORT"
|
||||
envDBUser = "MF_CLIENTS_DB_USER"
|
||||
envDBPass = "MF_CLIENTS_DB_PASS"
|
||||
envDBName = "MF_CLIENTS_DB"
|
||||
envHTTPPort = "MF_CLIENTS_HTTP_PORT"
|
||||
envGRPCPort = "MF_CLIENTS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envSecret = "MF_CLIENTS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
UsersURL string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
defer db.Close()
|
||||
|
||||
conn := connectToUsersService(cfg.UsersURL, logger)
|
||||
defer conn.Close()
|
||||
|
||||
svc := newService(conn, db, cfg.Secret, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Clients service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, secret string, logger log.Logger) clients.Service {
|
||||
users := usersapi.NewClient(conn)
|
||||
clientsRepo := postgres.NewClientRepository(db, logger)
|
||||
channelsRepo := postgres.NewChannelRepository(db, logger)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
|
||||
svc := clients.New(users, clientsRepo, channelsRepo, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "clients",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "clients",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Clients service started, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Clients gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/api"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort int = 5683
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port int
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: defPort,
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cc := clientsapi.NewClient(conn)
|
||||
|
||||
pubsub := nats.New(nc, logger)
|
||||
svc := coap.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "coap_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "coap_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%d", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("CoAP adapter service started, exposed port %d", cfg.Port))
|
||||
errs <- api.ListenAndServe(svc, cc, p)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("CoAP adapter terminated: %s", err))
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort string = "8180"
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defClientsURL string = "localhost:8181"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envClientsURL string = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cc := clientsapi.NewClient(conn)
|
||||
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)
|
||||
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/normalizer"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
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.New(os.Stdout)
|
||||
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("Normalizer service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "normalizer",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"})
|
||||
|
||||
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "normalizer",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"})
|
||||
|
||||
normalizer.Subscribe(nc, logger, counter, latency)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Normalizer service terminated: %s", err))
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/users/api"
|
||||
grpcapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
httpapi "github.com/mainflux/mainflux/users/api/http"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/jwt"
|
||||
"github.com/mainflux/mainflux/users/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defSecret = "users"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
envDBPort = "MF_USERS_DB_PORT"
|
||||
envDBUser = "MF_USERS_DB_USER"
|
||||
envDBPass = "MF_USERS_DB_PASS"
|
||||
envDBName = "MF_USERS_DB"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
defer db.Close()
|
||||
|
||||
svc := newService(db, cfg.Secret, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
logger.Error(fmt.Sprintf("Users service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func newService(db *sql.DB, secret string, logger log.Logger) users.Service {
|
||||
repo := postgres.New(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(secret)
|
||||
|
||||
svc := users.New(repo, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "users",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "users",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc users.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Users HTTP service started, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc users.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
mainflux.RegisterUsersServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Users gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
adapter "github.com/mainflux/mainflux/ws"
|
||||
"github.com/mainflux/mainflux/ws/api"
|
||||
"github.com/mainflux/mainflux/ws/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "8180"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defClientsURL = "localhost:8181"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envClientsURL = "MF_CLIENTS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ClientsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cc := clientsapi.NewClient(conn)
|
||||
pubsub := nats.New(nc)
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "ws_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "ws_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# Mainflux CoAP Adapter
|
||||
|
||||
Mainflux CoAP adapter provides an [CoAP](http://coap.technology/) API for sending messages through the
|
||||
platform.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-----------------------|------------------------|-------------------------|
|
||||
| MF_COAP_ADAPTER_PORT | adapter listening port | `5683` |
|
||||
| MF_NATS_URL | NATS instance URL | `nats://localhost:4222` |
|
||||
| MF_CLIENTS_URL | clients service URL | `http://localhost:8181` |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service is distributed as Docker container. The following snippet provides
|
||||
a compose file template that can be used to deploy the service container locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
adapter:
|
||||
image: mainflux/coap-adapter:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:[configured port]
|
||||
environment:
|
||||
MF_COAP_ADAPTER_PORT: [Service HTTP port]
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_CLIENTS_URL: [Manager service URL]
|
||||
```
|
||||
|
||||
Running this service outside of container requires working instance of the NATS service.
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the http
|
||||
make coap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-coap
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Since CoAP protocol does not support `Authorization` header (option), in order to send CoAP messages,
|
||||
client valid key must be present in `Uri-Query` option.
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Package coap contains the domain concept definitions needed to support
|
||||
// Mainflux coap adapter service functionality. All constant values are taken
|
||||
// from RFC, and could be adjusted based on specific use case.
|
||||
package coap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
responseBackoffMultiplier = 1.5
|
||||
|
||||
// AckTimeout is the amount of time to wait for a response.
|
||||
AckTimeout = int(2 * time.Second)
|
||||
|
||||
// MaxRetransmit is the maximum number of times a message will be retransmitted.
|
||||
MaxRetransmit = 4
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFailedMessagePublish indicates that message publishing failed.
|
||||
ErrFailedMessagePublish = errors.New("failed to publish message")
|
||||
|
||||
// ErrFailedSubscription indicates that client couldn't subscribe to specified channel.
|
||||
ErrFailedSubscription = errors.New("failed to subscribe to a channel")
|
||||
|
||||
// ErrFailedConnection indicates that service couldn't connect to message broker.
|
||||
ErrFailedConnection = errors.New("failed to connect to message broker")
|
||||
|
||||
// extracted to avoid recomputation
|
||||
maxTimeout = int(float64(AckTimeout) * ((math.Pow(2, float64(MaxRetransmit))) - 1) * responseBackoffMultiplier)
|
||||
)
|
||||
|
||||
// Service specifies coap service API.
|
||||
type Service interface {
|
||||
mainflux.MessagePublisher
|
||||
|
||||
// Subscribes to channel with specified id and adds subscription to
|
||||
// service map of subscriptions under given ID.
|
||||
Subscribe(string, string, nats.Channel) error
|
||||
|
||||
// Unsubscribe method is used to stop observing resource.
|
||||
Unsubscribe(string)
|
||||
|
||||
// SetTimeout sets timeout to wait CONF messages.
|
||||
SetTimeout(string, *time.Timer, int) (chan bool, error)
|
||||
|
||||
// RemoveTimeout removes timeout when ACK message is received from client
|
||||
// if timeout existed.
|
||||
RemoveTimeout(string)
|
||||
}
|
||||
|
||||
var _ Service = (*adapterService)(nil)
|
||||
|
||||
type adapterService struct {
|
||||
pubsub nats.Service
|
||||
subs map[string]nats.Channel
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// New instantiates the CoAP adapter implementation.
|
||||
func New(pubsub nats.Service) Service {
|
||||
return &adapterService{
|
||||
pubsub: pubsub,
|
||||
subs: make(map[string]nats.Channel),
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *adapterService) get(clientID string) (nats.Channel, bool) {
|
||||
svc.mu.Lock()
|
||||
obs, ok := svc.subs[clientID]
|
||||
svc.mu.Unlock()
|
||||
return obs, ok
|
||||
}
|
||||
|
||||
func (svc *adapterService) put(clientID string, obs nats.Channel) {
|
||||
svc.mu.Lock()
|
||||
svc.subs[clientID] = obs
|
||||
svc.mu.Unlock()
|
||||
}
|
||||
|
||||
func (svc *adapterService) remove(clientID string) {
|
||||
svc.mu.Lock()
|
||||
obs, ok := svc.subs[clientID]
|
||||
if ok {
|
||||
obs.Closed <- true
|
||||
delete(svc.subs, clientID)
|
||||
}
|
||||
svc.mu.Unlock()
|
||||
}
|
||||
|
||||
func (svc *adapterService) Publish(msg mainflux.RawMessage) error {
|
||||
if err := svc.pubsub.Publish(msg); err != nil {
|
||||
switch err {
|
||||
case broker.ErrConnectionClosed, broker.ErrInvalidConnection:
|
||||
return ErrFailedConnection
|
||||
default:
|
||||
return ErrFailedMessagePublish
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Subscribe(chanID, clientID string, ch nats.Channel) error {
|
||||
// Remove entry if already exists.
|
||||
svc.Unsubscribe(clientID)
|
||||
if err := svc.pubsub.Subscribe(chanID, ch); err != nil {
|
||||
return ErrFailedSubscription
|
||||
}
|
||||
svc.put(clientID, ch)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Unsubscribe(clientID string) {
|
||||
svc.remove(clientID)
|
||||
}
|
||||
|
||||
func (svc *adapterService) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
|
||||
sub, ok := svc.get(clientID)
|
||||
if !ok {
|
||||
return nil, errors.New("observer entry not found")
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-sub.Timer:
|
||||
timer.Stop()
|
||||
if ok {
|
||||
sub.Notify <- false
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
duration *= 2
|
||||
if duration >= maxTimeout {
|
||||
timer.Stop()
|
||||
sub.Notify <- false
|
||||
svc.Unsubscribe(clientID)
|
||||
return
|
||||
}
|
||||
timer.Reset(time.Duration(duration))
|
||||
sub.Notify <- true
|
||||
}
|
||||
}
|
||||
}()
|
||||
return sub.Notify, nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) RemoveTimeout(clientID string) {
|
||||
if sub, ok := svc.get(clientID); ok {
|
||||
sub.Timer <- false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -0,0 +1,66 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc coap.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the adapter.
|
||||
func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method publish to channel %s took %s to complete", msg.Channel, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Subscribe(chanID, clientID string, channel nats.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method subscribe to channel %s took %s to complete", chanID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Subscribe(chanID, clientID, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
|
||||
return lm.svc.SetTimeout(clientID, timer, duration)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveTimeout(clientID string) {
|
||||
lm.svc.RemoveTimeout(clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unsubscribe(clientID string) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Info(fmt.Sprintf("Method unsubscribe for client %s took %s to complete", clientID, time.Since(begin)))
|
||||
}(time.Now())
|
||||
lm.svc.Unsubscribe(clientID)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc coap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments adapter by tracking request count and latency.
|
||||
func MetricsMiddleware(svc coap.Service, counter metrics.Counter, latency metrics.Histogram) coap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "publish").Add(1)
|
||||
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Subscribe(chanID, clientID string, channel nats.Channel) error {
|
||||
return mm.svc.Subscribe(chanID, clientID, channel)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
|
||||
return mm.svc.SetTimeout(clientID, timer, duration)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveTimeout(clientID string) {
|
||||
mm.svc.RemoveTimeout(clientID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
|
||||
mm.svc.Unsubscribe(clientID)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
chanID = "id"
|
||||
keyHeader = "key"
|
||||
)
|
||||
|
||||
func authKey(opt interface{}) (string, error) {
|
||||
val, ok := opt.(string)
|
||||
if !ok {
|
||||
return "", errBadRequest
|
||||
}
|
||||
arr := strings.Split(val, "=")
|
||||
if len(arr) != 2 || strings.ToLower(arr[0]) != keyHeader {
|
||||
return "", errBadOption
|
||||
}
|
||||
return arr[1], nil
|
||||
}
|
||||
|
||||
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher *mainflux.Identity, err error) {
|
||||
if !govalidator.IsUUID(cid) {
|
||||
res.Code = gocoap.NotFound
|
||||
return
|
||||
}
|
||||
|
||||
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
|
||||
key, err := authKey(msg.Option(gocoap.URIQuery))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errBadOption:
|
||||
res.Code = gocoap.BadOption
|
||||
case errBadRequest:
|
||||
res.Code = gocoap.BadRequest
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
publisher, err = auth.CanAccess(ctx, &mainflux.AccessReq{Token: key, ChanID: cid})
|
||||
|
||||
if err != nil {
|
||||
e, ok := status.FromError(err)
|
||||
if ok {
|
||||
switch e.Code() {
|
||||
case codes.PermissionDenied:
|
||||
res.Code = gocoap.Forbidden
|
||||
default:
|
||||
res.Code = gocoap.ServiceUnavailable
|
||||
}
|
||||
return
|
||||
}
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func serve(svc coap.Service, conn *net.UDPConn, data []byte, addr *net.UDPAddr, rh gocoap.Handler) {
|
||||
msg, err := gocoap.ParseMessage(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case gocoap.Reset:
|
||||
if len(msg.Payload) != 0 {
|
||||
res.Code = gocoap.BadRequest
|
||||
break
|
||||
}
|
||||
cid := mux.Var(&msg, chanID)
|
||||
res.Type = gocoap.Acknowledgement
|
||||
publisher, err := authorize(&msg, res, cid)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
svc.RemoveTimeout(id)
|
||||
svc.Unsubscribe(id)
|
||||
case gocoap.Acknowledgement:
|
||||
cid := mux.Var(&msg, chanID)
|
||||
res.Type = gocoap.Acknowledgement
|
||||
publisher, err := authorize(&msg, res, cid)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
svc.RemoveTimeout(id)
|
||||
default:
|
||||
res = rh.ServeCOAP(conn, addr, &msg)
|
||||
}
|
||||
if res != nil && msg.IsConfirmable() {
|
||||
gocoap.Transmit(conn, addr, *res)
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe binds to the given address and serve requests forever.
|
||||
func ListenAndServe(svc coap.Service, csc mainflux.ClientsServiceClient, addr string) error {
|
||||
|
||||
handler := makeHandler(addr, svc)
|
||||
auth = csc
|
||||
uaddr, err := net.ResolveUDPAddr(network, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP(network, uaddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, maxPktLen)
|
||||
for {
|
||||
nr, addr, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if neterr, ok := err.(net.Error); ok && (neterr.Temporary() || neterr.Timeout()) {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
tmp := make([]byte, nr)
|
||||
copy(tmp, buf)
|
||||
go serve(svc, conn, tmp, addr, handler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
|
||||
"math/rand"
|
||||
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var (
|
||||
errBadRequest = errors.New("bad request")
|
||||
errBadOption = errors.New("bad option")
|
||||
auth mainflux.ClientsServiceClient
|
||||
)
|
||||
|
||||
const (
|
||||
maxPktLen = 1500
|
||||
network = "udp"
|
||||
protocol = "coap"
|
||||
)
|
||||
|
||||
const (
|
||||
// Approximately number of supported requests per second
|
||||
timestamp = int64(time.Millisecond) * 31
|
||||
)
|
||||
|
||||
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
|
||||
|
||||
// NotFoundHandler handles erroneously formed requests.
|
||||
func NotFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *gocoap.Message) *gocoap.Message {
|
||||
if m.IsConfirmable() {
|
||||
return &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.NotFound,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func version(port string) {
|
||||
b := bone.New()
|
||||
b.GetFunc("/version", mainflux.Version("CoAP"))
|
||||
http.ListenAndServe(port, b)
|
||||
}
|
||||
|
||||
func makeHandler(port string, svc coap.Service) gocoap.Handler {
|
||||
go version(port)
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
|
||||
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(observe(svc))).Methods(gocoap.GET)
|
||||
r.NotFoundHandler = gocoap.FuncHandler(NotFoundHandler)
|
||||
return r
|
||||
}
|
||||
|
||||
func receive(svc coap.Service) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
var res *gocoap.Message
|
||||
if msg.IsConfirmable() {
|
||||
res = &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
}
|
||||
|
||||
if len(msg.Payload) == 0 && msg.IsConfirmable() {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
|
||||
cid := mux.Var(msg, "id")
|
||||
publisher, err := authorize(msg, res, cid)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
rawMsg := mainflux.RawMessage{
|
||||
Channel: cid,
|
||||
Publisher: publisher.GetValue(),
|
||||
Protocol: protocol,
|
||||
Payload: msg.Payload,
|
||||
}
|
||||
|
||||
if err := svc.Publish(rawMsg); err != nil {
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func observe(svc coap.Service) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
var res *gocoap.Message
|
||||
if msg.IsConfirmable() {
|
||||
res = &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
}
|
||||
|
||||
cid := mux.Var(msg, "id")
|
||||
publisher, err := authorize(msg, res, cid)
|
||||
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 1 {
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
svc.Unsubscribe(id)
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
|
||||
ch := nats.Channel{
|
||||
Messages: make(chan mainflux.RawMessage),
|
||||
Closed: make(chan bool),
|
||||
Timer: make(chan bool),
|
||||
Notify: make(chan bool),
|
||||
}
|
||||
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
|
||||
if err := svc.Subscribe(cid, id, ch); err != nil {
|
||||
res.Code = gocoap.InternalServerError
|
||||
return res
|
||||
}
|
||||
go handleSub(svc, id, conn, addr, msg, ch)
|
||||
res.AddOption(gocoap.Observe, 0)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) error {
|
||||
buff := new(bytes.Buffer)
|
||||
now := time.Now().UnixNano() / timestamp
|
||||
if err := binary.Write(buff, binary.BigEndian, now); err != nil {
|
||||
return err
|
||||
}
|
||||
observeVal := buff.Bytes()
|
||||
msg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
|
||||
if msg.IsConfirmable() {
|
||||
timer := time.NewTimer(time.Duration(coap.AckTimeout))
|
||||
ch, err := svc.SetTimeout(id, timer, coap.AckTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go sendConfirmable(conn, addr, msg, ch)
|
||||
return nil
|
||||
}
|
||||
return gocoap.Transmit(conn, addr, *msg)
|
||||
}
|
||||
|
||||
func sendConfirmable(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch chan bool) {
|
||||
msg.SetOption(gocoap.MaxRetransmit, coap.MaxRetransmit)
|
||||
// Try to transmit MAX_RETRANSMITION times; every attempt duplicates timeout between transmission.
|
||||
for i := 0; i < coap.MaxRetransmit; i++ {
|
||||
if err := gocoap.Transmit(conn, addr, *msg); err != nil {
|
||||
return
|
||||
}
|
||||
state, ok := <-ch
|
||||
if !state || !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleSub(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch nats.Channel) {
|
||||
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
|
||||
// 24 hours. Since 24 hours is too long for our purposes, we use 12.
|
||||
ticker := time.NewTicker(12 * time.Hour)
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
res.SetOption(gocoap.LocationPath, msg.Path())
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ticker.Stop()
|
||||
res.Type = gocoap.Confirmable
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
if err := sendMessage(svc, id, conn, addr, res); err != nil {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
case rawMsg, ok := <-ch.Messages:
|
||||
if !ok {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
res.Type = gocoap.NonConfirmable
|
||||
res.Payload = rawMsg.Payload
|
||||
if err := sendMessage(svc, id, conn, addr, res); err != nil {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package nats contains NATS-specific message repository implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
const prefix = "channel"
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// New instantiates NATS message pubsub.
|
||||
func New(nc *broker.Conn, l log.Logger) Service {
|
||||
return &natsPublisher{nc, l}
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pubsub.nc.Publish(fmt.Sprintf("%s.%s", prefix, msg.Channel), data)
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Subscribe(chanID string, channel Channel) error {
|
||||
sub, err := pubsub.nc.Subscribe(fmt.Sprintf("%s.%s", prefix, chanID), func(msg *broker.Msg) {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
var rawMsg mainflux.RawMessage
|
||||
if err := proto.Unmarshal(msg.Data, &rawMsg); err != nil {
|
||||
return
|
||||
}
|
||||
channel.Messages <- rawMsg
|
||||
})
|
||||
|
||||
go func() {
|
||||
<-channel.Closed
|
||||
sub.Unsubscribe()
|
||||
channel.Close()
|
||||
}()
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package nats
|
||||
|
||||
import "github.com/mainflux/mainflux"
|
||||
|
||||
// Service specifies NATS service API.
|
||||
type Service interface {
|
||||
mainflux.MessagePublisher
|
||||
// Subscribe is used to subscribe to channel with specified id.
|
||||
Subscribe(string, Channel) error
|
||||
}
|
||||
|
||||
// Channel is used for receiving and sending messages.
|
||||
type Channel struct {
|
||||
Messages chan mainflux.RawMessage
|
||||
Closed chan bool
|
||||
Timer chan bool
|
||||
Notify chan bool
|
||||
}
|
||||
|
||||
// Close channel and stop message transfer.
|
||||
func (channel Channel) Close() {
|
||||
close(channel.Messages)
|
||||
close(channel.Closed)
|
||||
close(channel.Timer)
|
||||
close(channel.Notify)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "mainflux-ui"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"project": "src/tsconfig.app.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,58 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
/.vscode
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
|
||||
#System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#json-server database
|
||||
db.json
|
||||
db.*
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Mainflux UI Dashboad
|
||||
|
||||
Mainflux UI, dashboard for [Mainflux](https://github.com/mainflux/mainflux) Industrial IoT Messaging and Device Management Server.
|
||||
|
||||
> **N.B.** Mainflux UI service is WIP and not suitable for deployment at this moment. You are welcome to contribute and improve it.
|
||||
> ## Development
|
||||
>- Follow angular-cli [documentation](https://github.com/angular/angular-cli)
|
||||
>- Follow [official angular style guide](https://angular.io/styleguide)
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need the following software installed to get started.
|
||||
|
||||
- [Node](https://nodejs.org/en/) 6 or higher, we recommend current LTS version, together with NPM 3 or higher.
|
||||
- [Angular-cli](https://github.com/angular/angular-cli) Newest version with Webpack integration
|
||||
- - Depending on how Node is configured on your machine, you may need to run installation command with `sudo`
|
||||
- [Git](http://git-scm.com/downloads): Use the installer for your OS.
|
||||
- Windows users can also try [Git for Windows](http://git-for-windows.github.io/).
|
||||
- For local Development with [Mainflux composition](https://github.com/mainflux/mainflux) running locally, [Chrome extension for Cross origin](https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?utm_source=chrome-app-launcher-info-dialog) is required. Because composition is running on different port then our Angular app, we have cross origin.
|
||||
|
||||
## Configuration
|
||||
|
||||
Change into the directory.
|
||||
|
||||
```bash
|
||||
cd dashflux
|
||||
```
|
||||
|
||||
Install the dependencies. If you're running Mac OS or Linux, you may need to run `sudo npm install` instead, depending on how your machine is configured.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Set appropriate endpoint URLs in **environment.ts** (for local development will probably be 0.0.0.0:<_port_>) or **environment.prod.ts** for production.
|
||||
|
||||
To start the server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
This will run and assemble our app.
|
||||
**Now go to `localhost:4200` in your browser to see it in action.**
|
||||
|
||||
## Deployment
|
||||
|
||||
Dashflux is distributed as Docker container. We use nginx to serve dashflux from docker container, supporting environments using docker multi-stage builds.
|
||||
Dashflux docker image is available on [Dockerhub mainflux/dashflux](https://hub.docker.com/r/mainflux/dashflux/)
|
||||
|
||||
If you want to build image locally, you can build image using the **development** environment:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t dashflux:dev --build-arg env=dev .
|
||||
```
|
||||
|
||||
Build image using the **production** environment:
|
||||
|
||||
```bash
|
||||
docker build -t dashflux:prod -f ./docker/Dockerfile .
|
||||
```
|
||||
|
||||
**Note:** before running *docker build* command, please make sure appropriate endpoint URLs in *environment.ts* or *environment.prod.ts* are set up.
|
||||
|
||||
You can test image running
|
||||
|
||||
```bash
|
||||
docker run -p 80:80 dashflux:dev
|
||||
```
|
||||
|
||||
This will run dashflux in docker container.
|
||||
|
||||
Now go to `http://localhost` in your browser to see it in action.
|
||||
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
@@ -0,0 +1,13 @@
|
||||
# Stage 0, based on Node.js, to build and compile Angular
|
||||
FROM node:8.6 as node
|
||||
WORKDIR /app
|
||||
COPY package.json /app/
|
||||
RUN npm install
|
||||
COPY ./ /app/
|
||||
ARG env=prod
|
||||
RUN npm run build -- --prod --environment $env
|
||||
|
||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
||||
FROM nginx:1.13
|
||||
COPY --from=node /app/dist/ /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
@@ -0,0 +1,8 @@
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('mainflux-ui App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to app!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadless'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
Generated
+10394
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "mainflux-ui",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache License, version 2.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy-config.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.2.4",
|
||||
"@angular/common": "^5.2.4",
|
||||
"@angular/compiler": "^5.2.4",
|
||||
"@angular/core": "^5.2.4",
|
||||
"@angular/flex-layout": "2.0.0-beta.10-4905443",
|
||||
"@angular/forms": "^5.2.4",
|
||||
"@angular/http": "^5.2.4",
|
||||
"@angular/platform-browser": "^5.2.4",
|
||||
"@angular/platform-browser-dynamic": "^5.2.4",
|
||||
"@angular/router": "^5.2.4",
|
||||
"core-js": "^2.4.1",
|
||||
"mobx": "^3.5.1",
|
||||
"mobx-angular": "^2.1.1",
|
||||
"ngx-auth": "^2.2.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"zone.js": "^0.8.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cdk": "^5.2.0",
|
||||
"@angular/cli": "^1.7.3",
|
||||
"@angular/compiler-cli": "^5.2.4",
|
||||
"@angular/language-service": "^5.2.4",
|
||||
"@angular/material": "^5.2.0",
|
||||
"@types/jasmine": "~2.5.53",
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "^6.0.100",
|
||||
"codelyzer": "~3.2.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jasmine-core": "~2.5.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.2.0",
|
||||
"tslint": "~5.7.0",
|
||||
"typescript": "~2.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8180",
|
||||
"secure":false,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Route, RouterModule } from '@angular/router';
|
||||
import { ProtectedGuard, PublicGuard } from 'ngx-auth';
|
||||
|
||||
import { LoginComponent } from './components/auth/login/login.component';
|
||||
import { SignupComponent } from './components/auth/signup/signup.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { ClientsComponent } from './components/clients/clients.component';
|
||||
|
||||
const routes: Route[] = [
|
||||
{ path: '', redirectTo: 'clients', pathMatch: 'full'},
|
||||
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'clients', component: ClientsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -0,0 +1,15 @@
|
||||
<div *mobxAutorun>
|
||||
<div class="loading" *ngIf="uiStore.loading" fxLayoutAlign="center center">
|
||||
<mat-progress-spinner [mode]="'indeterminate'"></mat-progress-spinner>
|
||||
</div>
|
||||
<mat-toolbar color="primary" class="mat-elevation-z6">
|
||||
<h1 fxFlex>Mainflux</h1>
|
||||
<ng-container *ngIf="authStore.isAuthenticated">
|
||||
<a [routerLink]="['/clients']" mat-button>Clients</a>
|
||||
<a [routerLink]="['/channels']" mat-button>Channels</a>
|
||||
<a (click)="logout()" mat-button>Logout</a>
|
||||
</ng-container>
|
||||
|
||||
</mat-toolbar>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
.loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { MaterialModule } from './core/material/material.module';
|
||||
import { AuthenticationService } from './core/services/auth/authentication.service';
|
||||
import { TokenStorage } from './core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from './core/services/channels/channels.service';
|
||||
import { ClientsService } from './core/services/clients/clients.service';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { reaction } from 'mobx';
|
||||
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
public uiStore: UiStore,
|
||||
public authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
reaction(() => this.authStore.authError, (authError) => {
|
||||
if (authError) {
|
||||
this.snackBar.open(authError, '', {
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authStore.logout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import './rxjs-extensions.ts';
|
||||
import 'hammerjs';
|
||||
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MobxAngularModule } from 'mobx-angular';
|
||||
import { AUTH_SERVICE, AuthModule, PROTECTED_FALLBACK_PAGE_URI, PUBLIC_FALLBACK_PAGE_URI } from 'ngx-auth';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { LoginComponent } from './components/auth/login/login.component';
|
||||
import { SignupComponent } from './components/auth/signup/signup.component';
|
||||
import { AddChannelDialogComponent } from './components/channels/add-channel-dialog/add-channel-dialog.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.component';
|
||||
import { AddClientDialogComponent } from './components/clients/add-client-dialog/add-client-dialog.component';
|
||||
import { ClientsComponent } from './components/clients/clients.component';
|
||||
import { ConfirmationDialogComponent } from './components/shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { MaterialModule } from './core/material/material.module';
|
||||
import { AuthenticationService } from './core/services/auth/authentication.service';
|
||||
import { TokenStorage } from './core/services/auth/token-storage.service';
|
||||
import { ChannelsService } from './core/services/channels/channels.service';
|
||||
import { ClientsService } from './core/services/clients/clients.service';
|
||||
import { MockAuthService } from './core/services/mock-auth.service';
|
||||
import { MockChannelsService } from './core/services/mock-channels.service';
|
||||
import { MockClientsService } from './core/services/mock-clients.service';
|
||||
import { ChannelsStore } from './core/store/channels.store';
|
||||
import { ClientsStore } from './core/store/clients.store';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
import { UnauthorizedInterceptor } from './core/services/auth/unauthorized.interceptor';
|
||||
|
||||
export function factory(authenticationService: AuthenticationService) {
|
||||
return authenticationService;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ClientsComponent,
|
||||
ChannelsComponent,
|
||||
SignupComponent,
|
||||
LoginComponent,
|
||||
AddClientDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AuthModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
ReactiveFormsModule,
|
||||
MobxAngularModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
ClientsStore,
|
||||
ChannelsStore,
|
||||
AuthStore,
|
||||
MockAuthService,
|
||||
MockClientsService,
|
||||
MockChannelsService,
|
||||
ClientsService,
|
||||
ChannelsService,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
{ provide: PROTECTED_FALLBACK_PAGE_URI, useValue: '/' },
|
||||
{ provide: PUBLIC_FALLBACK_PAGE_URI, useValue: '/login' },
|
||||
{
|
||||
provide: AUTH_SERVICE,
|
||||
deps: [AuthenticationService],
|
||||
useFactory: factory
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [
|
||||
AddClientDialogComponent,
|
||||
AddChannelDialogComponent,
|
||||
ConfirmationDialogComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
|
||||
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Login</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" class="emailInput" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%">
|
||||
<input type="password" class="passwordInput" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="loginForm.get('password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="loginForm.invalid" mat-raised-button color="primary" class="loginButton">Login</button>
|
||||
<button mat-button (click)="signup()" class="signupButton">Sign Up with Email</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
|
||||
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
|
||||
import { LoginComponent } from './login.component';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call the store goToSignup when clicked on Sign Up with Email', inject([UiStore], (store: UiStore) => {
|
||||
const signupButton = fixture.debugElement.nativeElement.querySelector('.signupButton');
|
||||
const signupSpy = spyOn(store, 'goToSignup').and.stub();
|
||||
|
||||
signupButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(signupSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
loginForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.loginForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required]],
|
||||
});
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authStore.login(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.uiStore.goToSignup();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.loginForm.get('email').value,
|
||||
password: this.loginForm.get('password').value
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
|
||||
<form fxLayout="column" [formGroup]="signupForm" (ngSubmit)="signup()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Signup</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Repeat password" formControlName="repeatPassword" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.repeatPassword').errors?.required">
|
||||
Repeat the password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="signupForm.invalid" mat-raised-button color="primary" class="large-button">Signup</button>
|
||||
<a mat-button (click)="login()">Login</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user