Compare commits

..

144 Commits

Author SHA1 Message Date
dborovcanin 0ada09577e deploy: 168e8b90cb 2026-07-30 15:56:34 +00:00
dborovcanin 7a747fade7 deploy: 16ba29cf4a 2026-06-26 20:07:08 +00:00
dborovcanin 2d2d23b3ce deploy: 7f03134d8e 2026-05-08 08:35:18 +00:00
dborovcanin f18e96e990 deploy: 61d0427898 2026-04-06 13:23:53 +00:00
dborovcanin e95e0e4472 deploy: 791e084de6 2026-04-03 16:37:10 +00:00
dborovcanin 4f18a129b3 deploy: ef5c253c51 2026-04-01 07:55:20 +00:00
dborovcanin b78b1eb706 deploy: f8410b8940 2026-03-04 11:37:45 +00:00
dborovcanin ee2c0435fc deploy: 9c2608659f 2026-03-03 14:22:39 +00:00
dborovcanin 8fde65fee0 deploy: 2c476c17ee 2026-03-03 14:14:14 +00:00
dborovcanin 02b2facbb0 deploy: 4e77ae65e2 2026-02-04 16:14:00 +00:00
dborovcanin 01125764fb deploy: 59d9f91813 2026-01-23 13:43:06 +00:00
dborovcanin 41c9067328 deploy: 5b913dd46b 2026-01-22 07:55:36 +00:00
dborovcanin 7a6241a80d deploy: 28fc9738df 2026-01-19 11:10:56 +00:00
dborovcanin 7b1267e394 deploy: 67c28ff134 2025-12-31 09:44:02 +00:00
dborovcanin 2ae21cb38a deploy: 3e6b771850 2025-12-01 23:13:52 +00:00
dborovcanin 2380af8a85 deploy: 15e756a5a3 2025-12-01 17:53:16 +00:00
dborovcanin e289794cfa deploy: aa3cc3d7ee 2025-10-30 17:16:51 +00:00
Dušan Borovčanin 1a977ff4b1 Create CNAME 2025-10-30 18:07:14 +01:00
Dušan Borovčanin 3a0a958d22 Delete CNAME 2025-10-30 18:06:11 +01:00
Dušan Borovčanin 1c0b6623ad Update CNAME 2025-10-30 18:02:56 +01:00
dborovcanin 461f464329 deploy: 9650dde716 2025-10-30 16:37:20 +00:00
dborovcanin 391203effd deploy: 3e400da5ad 2025-10-15 08:26:10 +00:00
Dušan Borovčanin 86a63dce92 Update CNAME 2025-10-14 12:39:02 +02:00
dborovcanin d02df10d33 deploy: ac9bc9aad6 2025-10-06 08:49:10 +00:00
Dušan Borovčanin 9849b60d68 Update CNAME 2025-10-02 16:14:56 +02:00
dborovcanin fa2d4c13f4 deploy: ae7c93020a 2025-10-01 09:08:51 +00:00
dborovcanin dd9d6f0a88 deploy: b031dc08b8 2025-09-26 11:55:13 +00:00
dborovcanin d12b1b164a deploy: 61f1e9aca2 2025-09-18 10:18:17 +00:00
dborovcanin 88f5a60ebb deploy: e57ad79cd4 2025-09-05 13:24:25 +00:00
dborovcanin 7d6a8998b8 deploy: ec75f4eadb 2025-09-02 08:17:07 +00:00
dborovcanin bcf6eb5449 deploy: 0e1a090f16 2025-08-25 14:17:44 +00:00
dborovcanin 0c355c69f0 deploy: 4927333691 2025-08-20 13:57:11 +00:00
drasko f2eb2f76ef deploy: 8188da16b3 2025-08-13 12:48:29 +00:00
dborovcanin e72f7e700f deploy: 8e70e1113e 2025-07-28 10:04:13 +00:00
dborovcanin 56c5e6acc7 deploy: fa5b6b86c6 2025-07-07 08:43:27 +00:00
dborovcanin 639a945777 deploy: 16f1b411ec 2025-07-07 08:23:40 +00:00
dborovcanin 16471f4ab1 deploy: f0d014eba2 2024-12-19 17:30:11 +00:00
dborovcanin c2731d34b2 deploy: 9e6da193c8 2024-12-19 11:39:58 +00:00
dborovcanin f9383cd8b0 deploy: 901ed73fc9 2024-12-11 14:23:19 +00:00
dborovcanin b2e067341c deploy: cff356461d 2024-12-11 08:15:23 +00:00
dborovcanin cc99241a2b deploy: 1e77579b33 2024-12-10 10:56:03 +00:00
dborovcanin c9b214be86 deploy: 178aa46ad4 2024-12-04 10:17:32 +00:00
dborovcanin 7cef070f47 deploy: 10bc218dee 2024-12-03 16:18:20 +00:00
Dušan Borovčanin 375d6a83d7 Create CNAME 2024-12-03 16:26:24 +01:00
Dušan Borovčanin 11791c9582 Delete CNAME 2024-12-03 16:26:14 +01:00
Dušan Borovčanin 3c2a7c00ac Update CNAME 2024-12-03 16:25:47 +01:00
dborovcanin 6d1136c0a0 deploy: 252113aad6721729a3f0c47b711d4082484ca702 2024-12-03 15:01:05 +00:00
Dušan Borovčanin 7153e8aa4a Update CNAME 2024-12-03 15:54:59 +01:00
dborovcanin 7ef8d6f101 deploy: da1717cba3 2024-12-02 11:42:57 +00:00
dborovcanin 5c3d74a7e7 deploy: bb5f21b1f5 2024-11-26 20:20:17 +00:00
Dušan Borovčanin a3121a6850 Delete CNAME 2024-11-26 17:13:59 +01:00
dborovcanin ff0fba9914 deploy: 8d4e752274 2024-11-26 08:36:46 +00:00
dborovcanin 3159cfc32a deploy: b20b45023d 2024-11-25 12:02:44 +00:00
dborovcanin cc45fe5006 deploy: 3a02788e3a 2024-11-10 18:00:04 +00:00
dborovcanin d42287f43a deploy: a5059a7946 2024-11-08 17:31:19 +00:00
dborovcanin 96144cf37b deploy: 233bbf9861 2024-11-04 08:42:27 +00:00
dborovcanin 456adc3a6a deploy: 0019f71b46 2024-10-30 18:19:55 +00:00
dborovcanin 3cffdfe0c4 deploy: f88e11bdb2 2024-10-25 12:42:08 +00:00
dborovcanin 052db951e3 deploy: ca8ed3b006 2024-10-23 19:40:30 +00:00
dborovcanin f47ac0753e deploy: eb881690c7 2024-10-17 14:25:10 +00:00
dborovcanin 161ca011ee deploy: fdaecf54e5 2024-09-12 12:45:38 +00:00
dborovcanin f3bf4af82f deploy: 48c4a7e7d4 2024-09-03 14:50:49 +00:00
dborovcanin 53c95f1425 deploy: c398908d50 2024-07-22 11:13:06 +00:00
dborovcanin 96ea848b4f deploy: b49a2cd012 2024-07-09 11:03:28 +00:00
dborovcanin d9cbba5f23 deploy: 4e2ae73fb0 2024-07-02 20:50:09 +00:00
dborovcanin 5b99f1f137 deploy: 2fe2f850c2 2024-06-28 09:13:27 +00:00
dborovcanin 6641438d2a deploy: 0794363a3c 2024-06-27 14:38:41 +00:00
dborovcanin 3f8e14c593 deploy: b0e37dacf4 2024-06-26 20:12:18 +00:00
dborovcanin 93086ce204 deploy: 3355332ebd 2024-06-12 14:19:30 +00:00
dborovcanin dc41f3a5f1 deploy: 27f5fb8688 2024-06-12 10:07:56 +00:00
dborovcanin ef58136b85 deploy: d62982b4b6 2024-05-20 10:31:37 +00:00
dborovcanin 045517783f deploy: e72e8a663c5d1f8a889e8d44b4acc59dee6573aa 2024-05-20 10:05:56 +00:00
dborovcanin 98a21ad6d1 deploy: 75db28c522 2024-05-10 13:09:38 +00:00
dborovcanin 450dc6320b deploy: 19c1261e81 2024-04-29 14:08:14 +00:00
dborovcanin 5ebd3710e3 deploy: 07635ad828 2024-04-23 08:58:21 +00:00
dborovcanin 44a7e1cc89 deploy: 66c737a22d 2024-04-18 15:21:46 +00:00
dborovcanin b2f345f07c deploy: 54e322ee3e 2024-04-16 12:54:52 +00:00
dborovcanin 58670a8286 deploy: f334ee2606 2024-04-16 08:39:51 +00:00
dborovcanin 854b37724a deploy: c10c453eac 2024-04-12 15:41:04 +00:00
dborovcanin df182e3623 deploy: 3d0678725e 2024-04-12 15:05:24 +00:00
dborovcanin fad9f86a7f deploy: 76788d3ae2 2024-04-11 07:58:45 +00:00
dborovcanin 2a312d8b4b deploy: 658003080e 2024-04-10 10:02:53 +00:00
dborovcanin d736867bb5 deploy: e803f36f78 2024-04-10 08:19:12 +00:00
dborovcanin bd9dade57d deploy: 33704abc0c 2024-04-09 08:09:43 +00:00
dborovcanin 937231fe2d deploy: 5c62e339c1 2024-04-08 17:18:48 +00:00
dborovcanin 3043d938c1 deploy: eb6b201a2a 2024-04-03 11:01:58 +00:00
dborovcanin ecf9bb3dc1 deploy: a39d46238b 2024-04-03 09:55:26 +00:00
dborovcanin 0169fe9653 deploy: 65941368b8 2024-04-01 10:38:34 +00:00
Dušan Borovčanin 7b640b9d36 Update CNAME 2024-03-27 17:15:53 +01:00
dborovcanin 714f621be8 deploy: 70c3d65863 2024-03-22 15:16:24 +00:00
dborovcanin ad4cfc64ee deploy: 163ccbbe59 2024-03-08 09:46:07 +00:00
Dušan Borovčanin 296a315369 Update CNAME 2024-01-25 16:25:18 +01:00
Dušan Borovčanin 589c6d290b Update CNAME 2024-01-25 14:39:31 +01:00
drasko 4bd83baad5 deploy: cd82cc5a43 2023-10-17 13:38:29 +00:00
drasko bbbfc67c66 deploy: 09236e87a7 2023-10-16 11:43:27 +00:00
drasko 518495dfc7 deploy: 3d3aa525a6 2023-10-15 20:02:38 +00:00
drasko 0e4f98c0dc deploy: 896a74ad49 2023-08-09 10:26:46 +00:00
drasko 79fed471e3 deploy: 20fde26bbd 2023-08-01 15:21:23 +00:00
drasko 9d41c15920 deploy: b7b14cc8b6 2023-07-31 12:17:43 +00:00
dborovcanin c27a1cb982 deploy: aee0081864 2023-07-13 14:28:26 +00:00
drasko 321cb1d1dd deploy: 892015a60f 2023-07-07 09:15:17 +00:00
drasko 6a31b3c3bc deploy: 55e09c1921 2023-06-14 10:41:06 +00:00
drasko 9ff6dd25c0 deploy: 7cc1dd9f89 2023-05-24 22:13:57 +00:00
drasko a5cac6d108 deploy: 408eabaaa6 2023-05-24 22:03:10 +00:00
drasko 99e1f52c23 deploy: 7948aa72e8 2023-04-20 19:53:25 +00:00
drasko f4da6959d2 deploy: 5f9b3c9905 2023-01-24 14:10:53 +00:00
drasko e87b5f89ba deploy: d008ae5d97 2023-01-13 13:33:30 +00:00
dborovcanin 46ed0cc584 deploy: 47fd646030 2022-12-07 11:31:28 +00:00
dborovcanin 9e297c4a75 deploy: 8a4cc12cd3 2022-09-16 12:07:37 +00:00
drasko db09b536bf deploy: 721ee545f9 2022-08-11 16:59:19 +00:00
dusanb94 9e0226eda6 deploy: ce46723a8f 2022-03-30 09:19:33 +00:00
manuio 4629441c1f deploy: ce46723a8f 2022-03-29 09:53:54 +00:00
dusanb94 8502263d1f deploy: 29f9cbb10a 2022-03-23 12:20:27 +00:00
dusanb94 c5203886a1 deploy: 127044efad 2022-03-14 10:03:35 +00:00
drasko 0e0ff18466 deploy: f3ed852b36 2022-03-06 00:59:13 +00:00
dusanb94 3211e79a29 deploy: e5278c463f 2022-03-03 16:14:21 +00:00
drasko f1da6fb601 deploy: cc5d5195ab 2022-03-02 22:38:53 +00:00
drasko da4471eb63 deploy: 655ac838be 2022-02-09 20:26:01 +00:00
dusanb94 017ca584aa deploy: 1f8a221c22 2022-02-07 12:52:09 +00:00
drasko ea9d5857e7 deploy: 9e0947a355 2022-02-01 16:33:56 +00:00
dusanb94 74161fa2fd deploy: 81b2a1a2d3 2022-01-27 10:09:06 +00:00
manuio b4edae0070 deploy: 89061d33e5 2022-01-26 09:33:36 +00:00
drasko 077d9e53e6 deploy: 2abf9da27e 2022-01-25 18:43:16 +00:00
drasko 59c1ab659e deploy: 42dd813521 2022-01-24 20:19:19 +00:00
drasko b240a2dc2b deploy: bcc8cf7119 2022-01-19 15:46:42 +00:00
drasko f859efc984 deploy: bea09d97dd 2022-01-14 22:54:05 +00:00
drasko 9280d7b714 deploy: aa014c2191 2022-01-04 18:42:38 +00:00
drasko 8b1188a200 deploy: dd7d52ef10 2021-12-24 13:53:30 +00:00
drasko 13bf66ca8f deploy: b18c9e79dc 2021-12-14 10:17:50 +00:00
drasko 64bed5b859 deploy: 7e9ab453d4 2021-12-08 11:47:25 +00:00
drasko 79e3de997f deploy: 31d30b204d 2021-11-26 13:12:38 +00:00
drasko ed1b174a84 deploy: 3042d6b40b 2021-11-08 13:46:08 +00:00
drasko b9aee21eff deploy: b570c38ed0 2021-11-08 10:40:55 +00:00
drasko 73240ffedd deploy: 27d4646db4 2021-10-26 22:38:54 +00:00
drasko 7f7da3bb80 deploy: 5e9a91bd03 2021-10-15 22:26:47 +00:00
drasko e1fe70766e deploy: 94cba4aab2 2021-10-11 13:37:04 +00:00
drasko 641ada78da deploy: 66d3da0531 2021-10-06 12:34:51 +00:00
drasko 3b76ee386d deploy: 7b3c26f60f 2021-08-13 11:56:38 +00:00
drasko 56d8ff7a7c deploy: d73a5d53fe 2021-08-11 14:58:38 +00:00
drasko 276531fcf1 deploy: d6a3830ef4 2021-07-22 13:48:22 +00:00
dusanb94 2a3de350cc deploy: 5ac1203b55 2021-07-22 09:21:18 +00:00
drasko 04d614df43 deploy: 19f0437f57 2021-07-13 11:31:46 +00:00
drasko 2e49885404 deploy: af0162f0df 2021-07-08 11:52:03 +00:00
drasko 0e0a0b031c deploy: cddfdf4038 2021-06-23 11:38:02 +00:00
1299 changed files with 13462 additions and 343638 deletions
-1
View File
@@ -1 +0,0 @@
* @mainflux/maintainers
-88
View File
@@ -1,88 +0,0 @@
# 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.
-30
View File
@@ -1,30 +0,0 @@
<!--
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:
-4
View File
@@ -1,4 +0,0 @@
build
*.pb.go
site/
View File
+1
View File
@@ -0,0 +1 @@
docs.api.magistrala.absmach.eu
Generated
-397
View File
@@ -1,397 +0,0 @@
# 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 = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "61153c768f31ee5f130071d08fc82b85208528de"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/cisco/senml"
packages = ["."]
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
[[projects]]
branch = "master"
name = "github.com/containerd/continuity"
packages = ["pathdriver"]
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
packages = ["."]
revision = "e137e310a99955be01d60b2237da89153ea40987"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
branch = "master"
name = "github.com/docker/docker"
packages = [
"api/types",
"api/types/blkiodev",
"api/types/container",
"api/types/filters",
"api/types/mount",
"api/types/network",
"api/types/registry",
"api/types/strslice",
"api/types/swarm",
"api/types/swarm/runtime",
"api/types/versions",
"opts",
"pkg/archive",
"pkg/fileutils",
"pkg/homedir",
"pkg/idtools",
"pkg/ioutils",
"pkg/jsonmessage",
"pkg/longpath",
"pkg/mount",
"pkg/pools",
"pkg/stdcopy",
"pkg/system",
"pkg/term",
"pkg/term/windows"
]
revision = "72ba7f593fa4dbb628cf5ee83cd7daf955934cf5"
[[projects]]
name = "github.com/docker/go-connections"
packages = ["nat"]
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0"
[[projects]]
name = "github.com/docker/go-units"
packages = ["."]
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
version = "v0.3.2"
[[projects]]
branch = "master"
name = "github.com/dustin/go-coap"
packages = ["."]
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
[[projects]]
name = "github.com/fsouza/go-dockerclient"
packages = ["."]
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
version = "v1.2.0"
[[projects]]
name = "github.com/go-kit/kit"
packages = [
"endpoint",
"log",
"metrics",
"metrics/internal/lv",
"metrics/prometheus",
"transport/http"
]
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
version = "v0.6.0"
[[projects]]
name = "github.com/go-logfmt/logfmt"
packages = ["."]
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
version = "v0.3.0"
[[projects]]
name = "github.com/go-stack/stack"
packages = ["."]
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
version = "v1.7.0"
[[projects]]
name = "github.com/go-zoo/bone"
packages = ["."]
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
version = "1.2"
[[projects]]
name = "github.com/gogo/protobuf"
packages = ["proto"]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
name = "github.com/jinzhu/gorm"
packages = [
".",
"dialects/postgres"
]
revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166"
version = "v1.9.1"
[[projects]]
branch = "master"
name = "github.com/jinzhu/inflection"
packages = ["."]
revision = "04140366298a54a039076d798123ffa108fff46c"
[[projects]]
branch = "master"
name = "github.com/kr/logfmt"
packages = ["."]
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid"
]
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/nats-io/go-nats"
packages = [
".",
"encoders/builtin",
"util"
]
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
version = "v1.4.0"
[[projects]]
name = "github.com/nats-io/nuid"
packages = ["."]
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
version = "v1.0.0"
[[projects]]
name = "github.com/opencontainers/go-digest"
packages = ["."]
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
version = "v1.0.0-rc1"
[[projects]]
name = "github.com/opencontainers/image-spec"
packages = [
"specs-go",
"specs-go/v1"
]
revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1"
[[projects]]
name = "github.com/opencontainers/runc"
packages = [
"libcontainer/system",
"libcontainer/user"
]
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
version = "v0.1.1"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp"
]
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
[[projects]]
branch = "master"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
]
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
[[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 = "9831f2c3ac1068a78f50999a30db84270f647af6"
version = "v1.1"
[[projects]]
name = "go.uber.org/atomic"
packages = ["."]
revision = "8474b86a5a6f79c443ce4b2992817ff32cf208b8"
version = "v1.3.1"
[[projects]]
name = "go.uber.org/multierr"
packages = ["."]
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore"
]
revision = "35aad584952c3e7020db7b839f6b102de6271f89"
version = "v1.7.1"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish",
"ssh/terminal"
]
revision = "374053ea96cb300f8671b8d3b07edeeb06e203b4"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp"
]
revision = "24dd3780ca4f75fed9f321890729414a4b5d3f13"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "01acb38716e021ed1fc03a602bdb5838e1358c5e"
[[projects]]
name = "gopkg.in/ory-am/dockertest.v3"
packages = ["."]
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
version = "v3.1.6"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "c1b28e90e21e838cec2d16c95d8eb414b5de9bbe9667acb01b5375db047cc6db"
solver-name = "gps-cdcl"
solver-version = 1
-79
View File
@@ -1,79 +0,0 @@
[[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.0.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]]
name = "go.uber.org/zap"
version = "1.7.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"
[prune]
go-tests = true
unused-packages = true
-191
View File
@@ -1,191 +0,0 @@
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
View File
@@ -1,45 +0,0 @@
# Mainflux follows the timeless, highly efficient and totally unfair system
# known as [Benevolent dictator for
# life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
# Drasko DRASKOVIC in the role of BDFL.
[bdfl]
[[drasko]]
Name = "Drasko DRASKOVIC"
Email = "drasko.draskovic@mainflux.com"
GitHub = "drasko"
# However, this role serves only in dead-lock events, or in a special and very rare cases
# when BDFL completely disagrees with the decisions made.
# In the normal flow of events, decisions on the project design are made through discussions,
# most often on the Pull Requests.
#
# Maintainers have the special role in the project in managing and accepting PRs,
# overall leading the project and making design decisions on the maintained subsystems.
#
# A reference list of all maintainers of the Mainflux project.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[maintainers]
[[manuel]]
Name = "Manuel IMPERIALE"
Email = "manuel.imperiale@gmail.com"
GitHub = "manuIO"
[[janko]]
Name = "Janko ISIDOROVIC"
Email = "janko.isidorovic@mainflux.com"
GitHub = "janko-isidorovic"
[[nikola]]
Name = "Nikola MARCETIC"
Email = "nikola.marcetic@mainflux.com"
GitHub = "nmarcetic"
[[dejan]]
Name = "Dejan MIJIC"
Email = "dejan.mijic@mainflux.com"
GitHub = "mijicd"
-48
View File
@@ -1,48 +0,0 @@
BUILD_DIR = build
SERVICES = manager http normalizer ws
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=. *.proto
$(SERVICES): proto
$(call compile_service,$(@))
$(DOCKERS):
$(call make_docker,$(@))
dockers: $(DOCKERS)
latest: dockers
for svc in $(SERVICES); do \
docker push mainflux/$$svc; \
done
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
-67
View File
@@ -1,67 +0,0 @@
# 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
View File
@@ -0,0 +1,508 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala Alarms API
description: |
HTTP API for managing alarms service.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.5
servers:
- url: http://localhost:8050
- url: https://localhost:8050
tags:
- name: alarms
description: Everything about your Alarms
externalDocs:
description: Find out more about alarms
url: https://magistrala.absmach.eu/docs/
paths:
/{domainID}/alarms:
get:
operationId: listAlarms
summary: List Alarms
description: |
Retrieves a list of alarms with optional filtering
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Order'
- $ref: '#/components/parameters/Dir'
- $ref: '#/components/parameters/ChannelID'
- $ref: '#/components/parameters/ClientID'
- $ref: '#/components/parameters/Subtopic'
- $ref: '#/components/parameters/RuleID'
- $ref: '#/components/parameters/Status'
- $ref: '#/components/parameters/AssigneeID'
- $ref: '#/components/parameters/Severity'
- $ref: '#/components/parameters/UpdatedBy'
- $ref: '#/components/parameters/AssignedBy'
- $ref: '#/components/parameters/AcknowledgedBy'
- $ref: '#/components/parameters/ResolvedBy'
- $ref: '#/components/parameters/CreatedFrom'
- $ref: '#/components/parameters/CreatedTo'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/AlarmsPageRes'
'400':
description: Failed due to malformed query parameters
'401':
description: Missing or invalid access token
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
/{domainID}/alarms/{alarmID}:
get:
operationId: viewAlarm
summary: View Alarm
description: Retrieves an alarm by ID
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
responses:
'200':
$ref: '#/components/responses/AlarmRes'
'400':
description: Missing or invalid alarm ID
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
put:
operationId: updateAlarm
summary: Update Alarm
description: Updates an existing alarm
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
requestBody:
$ref: '#/components/requestBodies/AlarmUpdateReq'
responses:
'200':
$ref: '#/components/responses/AlarmRes'
'400':
description: Failed due to malformed JSON
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'415':
description: Missing or invalid content type
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
delete:
operationId: deleteAlarm
summary: Delete Alarm
description: Deletes an alarm
tags:
- alarms
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/AlarmID'
security:
- bearerAuth: []
responses:
'204':
description: Alarm deleted successfully
'400':
description: Failed due to malformed alarm ID
'401':
description: Missing or invalid access token
'403':
description: Failed to perform authorization over the entity
'404':
description: Alarm does not exist
'422':
description: Database can't process request
'500':
$ref: '#/components/responses/ServiceError'
/health:
get:
summary: Retrieves service health check info
tags:
- health
security: []
responses:
'200':
$ref: '#/components/responses/HealthRes'
'500':
$ref: '#/components/responses/ServiceError'
components:
schemas:
Alarm:
type: object
properties:
id:
type: string
description: Unique alarm identifier
readOnly: true
rule_id:
type: string
description: Rule ID that triggered this alarm
domain_id:
type: string
description: Domain ID this alarm belongs to
channel_id:
type: string
description: Channel ID where the alarm was triggered
client_id:
type: string
description: Client ID that triggered the alarm
subtopic:
type: string
description: Subtopic associated with the alarm
status:
type: string
description: Alarm status
enum: [active, cleared]
measurement:
type: string
description: Measurement that triggered the alarm
value:
type: string
description: Value that triggered the alarm
unit:
type: string
description: Unit of measurement
threshold:
type: string
description: Threshold value that was exceeded
cause:
type: string
description: Cause or description of the alarm
severity:
type: integer
description: Severity level (0-100)
minimum: 0
maximum: 100
assignee_id:
type: string
description: ID of the user assigned to this alarm
created_at:
type: string
format: date-time
description: Creation timestamp
readOnly: true
updated_at:
type: string
format: date-time
description: Last update timestamp
readOnly: true
updated_by:
type: string
description: User who last updated the alarm
readOnly: true
assigned_at:
type: string
format: date-time
description: When the alarm was assigned
readOnly: true
assigned_by:
type: string
description: User who assigned the alarm
readOnly: true
acknowledged_at:
type: string
format: date-time
description: When the alarm was acknowledged
readOnly: true
acknowledged_by:
type: string
description: User who acknowledged the alarm
readOnly: true
resolved_at:
type: string
format: date-time
description: When the alarm was resolved
readOnly: true
resolved_by:
type: string
description: User who resolved the alarm
readOnly: true
metadata:
type: object
description: Custom metadata
additionalProperties: true
AlarmsPage:
type: object
properties:
offset:
type: integer
description: Number of items to skip during retrieval
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve
minimum: 1
maximum: 1000
default: 10
total:
type: integer
description: Total number of results
minimum: 0
alarms:
type: array
minItems: 0
items:
$ref: '#/components/schemas/Alarm'
required:
- alarms
- total
- offset
- limit
parameters:
DomainID:
name: domainID
description: Domain ID
in: path
required: true
schema:
type: string
AlarmID:
name: alarmID
description: Alarm ID
in: path
required: true
schema:
type: string
Offset:
name: offset
description: Number of items to skip
in: query
required: false
schema:
type: integer
default: 0
minimum: 0
Limit:
name: limit
description: Size of the subset to retrieve
in: query
required: false
schema:
type: integer
default: 10
minimum: 1
maximum: 1000
Order:
name: order
description: Order by field
in: query
required: false
schema:
type: string
enum: [created_at, updated_at]
default: created_at
Dir:
name: dir
description: Sort direction
in: query
required: false
schema:
type: string
enum: [asc, desc]
default: desc
ChannelID:
name: channel_id
description: Filter by channel ID
in: query
required: false
schema:
type: string
ClientID:
name: client_id
description: Filter by client ID
in: query
required: false
schema:
type: string
Subtopic:
name: subtopic
description: Filter by subtopic
in: query
required: false
schema:
type: string
RuleID:
name: rule_id
description: Filter by rule ID
in: query
required: false
schema:
type: string
Status:
name: status
description: Filter by alarm status
in: query
required: false
schema:
type: string
enum: [active, cleared, all]
default: all
AssigneeID:
name: assignee_id
description: Filter by assignee ID
in: query
required: false
schema:
type: string
Severity:
name: severity
description: Filter by severity level
in: query
required: false
schema:
type: integer
minimum: 0
maximum: 100
UpdatedBy:
name: updated_by
description: Filter by user who updated
in: query
required: false
schema:
type: string
AssignedBy:
name: assigned_by
description: Filter by user who assigned
in: query
required: false
schema:
type: string
AcknowledgedBy:
name: acknowledged_by
description: Filter by user who acknowledged
in: query
required: false
schema:
type: string
ResolvedBy:
name: resolved_by
description: Filter by user who resolved
in: query
required: false
schema:
type: string
CreatedFrom:
name: created_from
description: Filter alarms created after this time (RFC3339 format)
in: query
required: false
schema:
type: string
format: date-time
CreatedTo:
name: created_to
description: Filter alarms created before this time (RFC3339 format)
in: query
required: false
schema:
type: string
format: date-time
requestBodies:
AlarmUpdateReq:
description: JSON-formatted document describing the alarm update
required: true
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: Alarm status
enum: [active, cleared]
assignee_id:
type: string
description: ID of the user assigned to this alarm
severity:
type: integer
description: Severity level (0-100)
minimum: 0
maximum: 100
metadata:
type: object
description: Custom metadata
additionalProperties: true
responses:
AlarmRes:
description: Alarm data retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/Alarm'
links:
update:
operationId: updateAlarm
parameters:
alarmID: $response.body#/id
domainID: $response.body#/domain_id
delete:
operationId: deleteAlarm
parameters:
alarmID: $response.body#/id
domainID: $response.body#/domain_id
AlarmsPageRes:
description: Alarms page retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/AlarmsPage'
ServiceError:
description: Unexpected server-side error occurred
HealthRes:
description: Service Health Check
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
+851
View File
@@ -0,0 +1,851 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Magistrala Auth Service
description: |
This is the Auth Server based on the OpenAPI 3.0 specification. It is the HTTP API for managing platform users. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.0
servers:
- url: http://localhost:9001
- url: https://localhost:9001
tags:
- name: Keys
description: Everything about your Keys.
externalDocs:
description: Find out more about keys
url: https://magistrala.absmach.eu/docs/
- name: PATs
description: Everything about your Personal Access Tokens.
externalDocs:
description: Find out more about Personal Access Tokens
url: https://magistrala.absmach.eu/docs/
- name: Health
description: Service health check endpoint.
externalDocs:
description: Find out more about health check
url: https://magistrala.absmach.eu/docs/
paths:
/keys:
post:
operationId: issueKey
tags:
- Keys
summary: Issue API key
description: |
Generates a new API key. Thew new API key will
be uniquely identified by its ID.
requestBody:
$ref: "#/components/requestBodies/KeyRequest"
responses:
"201":
description: Issued new key.
"400":
description: Failed due to malformed JSON.
"401":
description: Missing or invalid access token provided.
"409":
description: Failed due to using already existing ID.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/keys/{keyID}:
get:
operationId: getKey
summary: Gets API key details.
description: |
Gets API key details for the given key.
tags:
- Keys
parameters:
- $ref: "#/components/parameters/ApiKeyId"
responses:
"200":
$ref: "#/components/responses/KeyRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: revokeKey
summary: Revoke API key
description: |
Revoke API key identified by the given ID.
tags:
- Keys
parameters:
- $ref: "#/components/parameters/ApiKeyId"
responses:
"204":
description: Key revoked.
"401":
description: Missing or invalid access token provided.
"404":
description: A non-existent entity request.
"422":
description: Service can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/pats:
post:
operationId: createPAT
tags:
- PATs
summary: Create a new Personal Access Token
description: |
Creates a new Personal Access Token (PAT) for the authenticated user.
requestBody:
$ref: "#/components/requestBodies/CreatePATRequest"
responses:
"201":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: listPATs
tags:
- PATs
summary: List all Personal Access Tokens
description: |
Lists all Personal Access Tokens (PATs) for the authenticated user.
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/PATsPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllPATs
tags:
- PATs
summary: Remove all Personal Access Tokens
description: |
Removes all Personal Access Tokens (PATs) for the authenticated user.
responses:
"200":
description: All PATs removed successfully.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}:
get:
operationId: retrievePAT
tags:
- PATs
summary: Retrieve a Personal Access Token
description: |
Retrieves details of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"200":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: deletePAT
tags:
- PATs
summary: Delete a Personal Access Token
description: |
Deletes a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"204":
description: PAT deleted successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/name:
patch:
operationId: updatePATName
tags:
- PATs
summary: Update Personal Access Token name
description: |
Updates the name of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/UpdatePATNameRequest"
responses:
"202":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/description:
patch:
operationId: updatePATDescription
tags:
- PATs
summary: Update Personal Access Token description
description: |
Updates the description of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/UpdatePATDescriptionRequest"
responses:
"202":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/reset:
patch:
operationId: resetPATSecret
tags:
- PATs
summary: Reset Personal Access Token secret
description: |
Resets the secret of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/ResetPATSecretRequest"
responses:
"200":
$ref: "#/components/responses/PATRes"
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/secret/revoke:
patch:
operationId: revokePATSecret
tags:
- PATs
summary: Revoke Personal Access Token secret
description: |
Revokes the secret of a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"204":
description: PAT secret revoked successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope:
get:
operationId: listScopes
tags:
- PATs
summary: List scopes for a Personal Access Token
description: |
Lists all scopes for a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
"200":
$ref: "#/components/responses/ScopesPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: clearAllScopes
tags:
- PATs
summary: Remove all scopes from a Personal Access Token
description: |
Removes all scopes from a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
responses:
"200":
description: All scopes removed successfully.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/add:
patch:
operationId: addScope
tags:
- PATs
summary: Add scope to a Personal Access Token
description: |
Adds a scope to a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/AddScopeRequest"
responses:
"200":
description: Scope added successfully.
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"422":
description: Database cannot process the request.
"500":
$ref: "#/components/responses/ServiceError"
/pats/{patID}/scope/remove:
patch:
operationId: removeScope
tags:
- PATs
summary: Remove scope from a Personal Access Token
description: |
Removes a scope from a specific Personal Access Token (PAT).
parameters:
- $ref: "#/components/parameters/PatID"
requestBody:
$ref: "#/components/requestBodies/RemoveScopeRequest"
responses:
"200":
description: Scope removed successfully.
"400":
description: Failed due to malformed JSON or validation errors.
"401":
description: Missing or invalid access token provided.
"404":
description: PAT not found.
"415":
description: Missing or invalid content type.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- Health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
PAT:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: Personal Access Token unique identifier
user_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: User ID of the PAT owner
name:
type: string
example: "My PAT"
description: Name of the Personal Access Token
description:
type: string
example: "Token for automation"
description: Description of the Personal Access Token
secret:
type: string
example: "pat_1234567890abcdef"
description: Secret value of the Personal Access Token
issued_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was issued
expires_at:
type: string
format: date-time
example: "2020-11-26T13:31:52Z"
description: Time when the PAT expires
updated_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was last updated
last_used_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was last used
revoked:
type: boolean
example: false
description: Whether the PAT is revoked
revoked_at:
type: string
format: date-time
example: "2019-11-26T13:31:52Z"
description: Time when the PAT was revoked
PATsPage:
type: object
properties:
total:
type: integer
example: 10
description: Total number of PATs
offset:
type: integer
example: 0
description: Number of items to skip during retrieval
limit:
type: integer
example: 10
description: Size of the subset to retrieve
pats:
type: array
items:
$ref: "#/components/schemas/PAT"
description: List of Personal Access Tokens
Scope:
type: object
properties:
optional_domain_id:
type: string
format: uuid
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
description: Optional domain ID for the scope
entity_type:
type: string
enum:
[groups, channels, clients, domains, users, dashboards, messages]
example: "groups"
description: Type of entity the scope applies to
entity_id:
type: string
example: "*"
description: ID of the entity the scope applies to. '*' means all entities of the specified type.
operation:
type: string
enum:
[
create,
read,
list,
update,
delete,
share,
unshare,
publish,
subscribe,
]
example: "read"
description: Operation allowed by this scope
ScopesPage:
type: object
properties:
total:
type: integer
example: 10
description: Total number of scopes
offset:
type: integer
example: 0
description: Number of items to skip during retrieval
limit:
type: integer
example: 10
description: Size of the subset to retrieve
scopes:
type: array
items:
$ref: "#/components/schemas/Scope"
description: List of scopes
Key:
type: object
properties:
id:
type: string
format: uuid
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
description: API key unique identifier
issuer_id:
type: string
format: uuid
example: "9118de62-c680-46b7-ad0a-21748a52833a"
description: In ID of the entity that issued the token.
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
subject:
type: string
format: string
example: "test@example.com"
description: User's email or service identifier of API key subject.
issued_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the key is generated.
expires_at:
type: string
format: date-time
example: "2019-11-26 13:31:52"
description: Time when the Key expires. If this field is missing,
that means that Key is valid indefinitely.
parameters:
PatID:
name: patID
description: Personal Access Token ID.
in: path
schema:
type: string
format: uuid
required: true
DomainID:
name: domainID
description: Unique domain identifier.
in: path
schema:
type: string
format: uuid
required: true
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
Status:
name: status
description: Domain status.
in: query
schema:
type: string
default: enabled
required: false
example: enabled
DomainName:
name: name
description: Domain's name.
in: query
schema:
type: string
required: false
example: "domainName"
Permission:
name: permission
description: permission.
in: query
schema:
type: string
required: false
example: "edit"
ApiKeyId:
name: keyID
description: API Key ID.
in: path
schema:
type: string
format: uuid
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
schema:
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Metadata:
name: metadata
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
in: query
required: false
schema:
type: object
additionalProperties: {}
Type:
name: type
description: The type of the API Key.
in: query
schema:
type: integer
default: 0
minimum: 0
required: false
Subject:
name: subject
description: The subject of an API Key
in: query
schema:
type: string
required: false
requestBodies:
CreatePATRequest:
description: JSON-formatted document describing PAT creation request.
required: true
content:
application/json:
schema:
type: object
required:
- name
- duration
properties:
name:
type: string
minLength: 1
example: "My PAT"
description: Name of the Personal Access Token
description:
type: string
example: "Token for automation"
description: Description of the Personal Access Token
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the PAT is valid. Format is a duration string (e.g. "30d", "24h", "1y").
UpdatePATNameRequest:
description: JSON-formatted document describing PAT name update request.
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
example: "New PAT Name"
description: New name for the Personal Access Token
UpdatePATDescriptionRequest:
description: JSON-formatted document describing PAT description update request.
required: true
content:
application/json:
schema:
type: object
required:
- description
properties:
description:
type: string
example: "New PAT Description"
description: New description for the Personal Access Token
ResetPATSecretRequest:
description: JSON-formatted document describing PAT secret reset request.
required: true
content:
application/json:
schema:
type: object
required:
- duration
properties:
duration:
type: string
pattern: "^[0-9]+(ns|us|µs|ms|s|m|h|d|w|y)$"
example: "30d"
description: Duration for which the new PAT secret is valid. Format is a duration string (e.g. "30d", "24h", "1y").
AddScopeRequest:
description: JSON-formatted document describing add scope request.
required: true
content:
application/json:
schema:
type: object
required:
- scopes
properties:
scopes:
type: array
items:
$ref: "#/components/schemas/Scope"
description: List of scopes to add
RemoveScopeRequest:
description: JSON-formatted document describing remove scope request.
required: true
content:
application/json:
schema:
type: object
required:
- scopes_id
properties:
scopes_id:
type: array
items:
type: string
format: uuid
description: List of scope IDs to remove
KeyRequest:
description: JSON-formatted document describing key request.
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: integer
example: 0
description: API key type. Keys of different type are processed differently.
duration:
type: number
format: integer
example: 23456
description: Number of seconds issued token is valid for.
responses:
PATRes:
description: Personal Access Token data.
content:
application/json:
schema:
$ref: "#/components/schemas/PAT"
PATsPageRes:
description: Page of Personal Access Tokens.
content:
application/json:
schema:
$ref: "#/components/schemas/PATsPage"
ScopesPageRes:
description: Page of scopes.
content:
application/json:
schema:
$ref: "#/components/schemas/ScopesPage"
ServiceError:
description: Unexpected server-side error occurred.
KeyRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: "#/components/schemas/Key"
links:
revoke:
operationId: revokeKey
parameters:
keyID: $response.body#/id
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
* Users access: "Authorization: Bearer <user_token>"
security:
- bearerAuth: []
+1151
View File
File diff suppressed because it is too large Load Diff
+722
View File
@@ -0,0 +1,722 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.3
info:
title: Certs Service API
description: |
Certificate management service for issuing, renewing, revoking, and managing X.509 certificates.
This service provides PKI functionality including certificate lifecycle management, OCSP responder,
and CRL generation.
version: 1.0.0
contact:
name: Abstract Machines
license:
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://localhost:9019
description: Development server
tags:
- name: certificates
description: Certificate lifecycle management operations
- name: pki
description: PKI infrastructure operations (OCSP, CRL, CA)
- name: health
description: Service health and monitoring
security:
- BearerAuth: []
paths:
/{domainID}/certs/issue/{entityID}:
post:
tags:
- certificates
summary: Issue a new certificate
description: Issues a new X.509 certificate for the specified entity with custom subject options
operationId: issueCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueCertRequest'
responses:
'201':
description: Certificate successfully issued
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}/renew:
patch:
tags:
- certificates
summary: Renew a certificate
description: Renews an existing certificate with extended TTL and new serial number
operationId: renewCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'200':
description: Certificate successfully renewed
content:
application/json:
schema:
$ref: '#/components/schemas/RenewCertResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}/revoke:
patch:
tags:
- certificates
summary: Revoke a certificate
description: Revokes a certificate by its serial number
operationId: revokeCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'204':
description: Certificate successfully revoked
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{entityID}/delete:
delete:
tags:
- certificates
summary: Delete certificates for an entity
description: Deletes all certificates associated with the specified entity
operationId: deleteCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
responses:
'204':
description: Certificates successfully deleted
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/UnprocessableEntity'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs:
get:
tags:
- certificates
summary: List certificates
description: Retrieves a paginated list of certificates with optional filtering by entity ID
operationId: listCerts
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/EntityIDFilter'
responses:
'200':
description: Certificates successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/CertificateListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/{id}:
get:
tags:
- certificates
summary: View certificate details
description: Retrieves detailed information about a specific certificate by serial number
operationId: viewCert
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/CertID'
responses:
'200':
description: Certificate details successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ViewCertResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/{domainID}/certs/csrs/{entityID}:
post:
tags:
- certificates
summary: Issue certificate from CSR
description: Issues a certificate from a Certificate Signing Request (CSR)
operationId: issueFromCSR
security:
- BearerAuth: []
parameters:
- $ref: '#/components/parameters/DomainID'
- $ref: '#/components/parameters/EntityID'
- $ref: '#/components/parameters/TTL'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRRequest'
responses:
'200':
description: Certificate successfully issued from CSR
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/csrs/{entityID}:
post:
tags:
- certificates
summary: Issue certificate from CSR (Internal)
description: Issues a certificate from a CSR using internal agent authentication
operationId: issueFromCSRInternal
security:
- AgentAuth: []
parameters:
- $ref: '#/components/parameters/EntityID'
- $ref: '#/components/parameters/TTL'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRRequest'
responses:
'200':
description: Certificate successfully issued from CSR
content:
application/json:
schema:
$ref: '#/components/schemas/IssueFromCSRResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/ocsp:
post:
tags:
- pki
summary: OCSP responder
description: |
Online Certificate Status Protocol (OCSP) responder endpoint.
Accepts both binary OCSP requests and JSON format requests.
operationId: ocsp
security: []
parameters:
- name: force_status
in: query
description: Force a specific OCSP status for testing
required: false
schema:
type: string
requestBody:
required: true
content:
application/ocsp-request:
schema:
type: string
format: binary
description: DER-encoded OCSP request
application/json:
schema:
$ref: '#/components/schemas/OCSPRequest'
responses:
'200':
description: OCSP response
content:
application/ocsp-response:
schema:
type: string
format: binary
description: DER-encoded OCSP response
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/crl:
get:
tags:
- pki
summary: Generate Certificate Revocation List
description: Generates and returns the current Certificate Revocation List (CRL)
operationId: generateCRL
security: []
responses:
'200':
description: CRL successfully generated
content:
application/json:
schema:
$ref: '#/components/schemas/CRLResponse'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/view-ca:
get:
tags:
- pki
summary: View CA certificate
description: Retrieves the CA certificate chain (root and intermediate certificates)
operationId: viewCA
security: []
responses:
'200':
description: CA certificate successfully retrieved
content:
application/json:
schema:
$ref: '#/components/schemas/ViewCertResponse'
'500':
$ref: '#/components/responses/InternalServerError'
/certs/download-ca:
get:
tags:
- pki
summary: Download CA certificate
description: Downloads the CA certificate as a ZIP file
operationId: downloadCA
security: []
responses:
'200':
description: CA certificate ZIP file
content:
application/zip:
schema:
type: string
format: binary
'500':
$ref: '#/components/responses/InternalServerError'
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
'200':
$ref: '#/components/responses/HealthRes'
'500':
$ref: '#/components/responses/InternalServerError'
/metrics:
get:
tags:
- health
summary: Prometheus metrics
description: Returns Prometheus metrics for monitoring
operationId: metrics
security: []
responses:
'200':
description: Metrics successfully retrieved
content:
text/plain:
schema:
type: string
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: User authentication token
AgentAuth:
type: http
scheme: bearer
description: Agent authentication token for internal operations
parameters:
DomainID:
name: domainID
in: path
required: true
description: Domain identifier
schema:
type: string
EntityID:
name: entityID
in: path
required: true
description: Entity identifier for the certificate
schema:
type: string
CertID:
name: id
in: path
required: true
description: Certificate serial number
schema:
type: string
Offset:
name: offset
in: query
description: Number of items to skip
schema:
type: integer
minimum: 0
default: 0
Limit:
name: limit
in: query
description: Maximum number of items to return
schema:
type: integer
minimum: 1
maximum: 100
default: 10
EntityIDFilter:
name: entity_id
in: query
description: Filter certificates by entity ID
schema:
type: string
TTL:
name: ttl
in: query
description: Time to live for the certificate (e.g., "8760h", "365d")
schema:
type: string
schemas:
IssueCertRequest:
type: object
required:
- options
properties:
ttl:
type: string
description: Time to live for the certificate (e.g., "8760h" for 1 year)
example: "8760h"
ip_addresses:
type: array
items:
type: string
description: IP addresses to include in the certificate
example: ["192.168.1.1", "10.0.0.1"]
options:
$ref: '#/components/schemas/SubjectOptions'
SubjectOptions:
type: object
required:
- common_name
properties:
common_name:
type: string
description: Common Name (CN) for the certificate subject
example: "example.com"
organization:
type: array
items:
type: string
description: Organization (O)
example: ["Abstract Machines"]
organizational_unit:
type: array
items:
type: string
description: Organizational Unit (OU)
example: ["Engineering"]
country:
type: array
items:
type: string
description: Country (C)
example: ["US"]
province:
type: array
items:
type: string
description: Province or State (ST)
example: ["California"]
locality:
type: array
items:
type: string
description: Locality or City (L)
example: ["San Francisco"]
street_address:
type: array
items:
type: string
description: Street Address
example: ["123 Main St"]
postal_code:
type: array
items:
type: string
description: Postal Code
example: ["94105"]
dns_names:
type: array
items:
type: string
description: DNS names for Subject Alternative Names
example: ["example.com", "www.example.com"]
ip_addresses:
type: array
items:
type: string
description: IP addresses for Subject Alternative Names
example: ["192.168.1.1"]
CertificateResponse:
type: object
properties:
serial_number:
type: string
description: Unique serial number of the certificate
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
key:
type: string
description: PEM-encoded private key
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
revoked:
type: boolean
description: Whether the certificate is revoked
example: false
expiry_time:
type: string
format: date-time
description: Certificate expiration time
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Entity identifier associated with the certificate
example: "entity-123"
RenewCertResponse:
type: object
properties:
certificate:
$ref: '#/components/schemas/ViewCertResponse'
ViewCertResponse:
type: object
properties:
serial_number:
type: string
description: Certificate serial number
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
key:
type: string
description: PEM-encoded private key
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
revoked:
type: boolean
description: Revocation status
example: false
expiry_time:
type: string
format: date-time
description: Expiration timestamp
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Associated entity identifier
example: "entity-123"
CertificateListResponse:
type: object
properties:
total:
type: integer
format: uint64
description: Total number of certificates
example: 100
offset:
type: integer
format: uint64
description: Current offset
example: 0
limit:
type: integer
format: uint64
description: Current limit
example: 10
certificates:
type: array
items:
$ref: '#/components/schemas/ViewCertResponse'
IssueFromCSRRequest:
type: object
required:
- csr
properties:
csr:
type: string
format: byte
description: PEM-encoded Certificate Signing Request
example: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K..."
IssueFromCSRResponse:
type: object
properties:
serial_number:
type: string
description: Serial number of the issued certificate
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
revoked:
type: boolean
description: Revocation status
example: false
expiry_time:
type: string
format: date-time
description: Expiration timestamp
example: "2026-11-05T12:00:00Z"
entity_id:
type: string
description: Associated entity identifier
example: "entity-123"
OCSPRequest:
type: object
properties:
serial_number:
type: string
description: Certificate serial number to check
example: "4a:3f:5e:2c:1b:8d:9e:7f"
certificate:
type: string
description: PEM-encoded certificate to check
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
status:
type: string
description: Force a specific status (for testing)
enum: [good, revoked, unknown]
CRLResponse:
type: object
properties:
crl:
type: string
format: byte
description: DER-encoded Certificate Revocation List
Error:
type: object
properties:
error:
type: string
description: Error message
example: "invalid request"
responses:
BadRequest:
description: Bad request - invalid parameters or malformed request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized - invalid or missing authentication token
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
UnprocessableEntity:
description: Unprocessable entity - request cannot be processed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: './schemas/health_info.yaml'
+1149
View File
File diff suppressed because it is too large Load Diff
+1585
View File
File diff suppressed because it is too large Load Diff
-83
View File
@@ -1,83 +0,0 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
broker "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
const (
port int = 5683
defNatsURL string = broker.DefaultURL
envNatsURL string = "COAP_ADAPTER_NATS_URL"
)
type config struct {
Port int
NatsURL string
}
func main() {
cfg := loadConfig()
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
nc := connectToNats(cfg, logger)
defer nc.Close()
pub := nats.NewMessagePublisher(nc)
ca := adapter.NewCoAPAdapter(logger, pub)
nc.Subscribe("src.http", ca.BridgeHandler)
nc.Subscribe("src.mqtt", ca.BridgeHandler)
errs := make(chan error, 2)
go func() {
coapAddr := fmt.Sprintf(":%d", cfg.Port)
errs <- ca.Serve(coapAddr)
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
c := <-errs
logger.Info("terminated", zap.String("error", c.Error()))
}
func loadConfig() *config {
return &config{
NatsURL: env(envNatsURL, defNatsURL),
Port: port,
}
}
func env(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func connectToNats(cfg *config, logger *zap.Logger) *broker.Conn {
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Error("Failed to connect to NATS", zap.Error(err))
os.Exit(1)
}
return nc
}
-89
View File
@@ -1,89 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
adapter "github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/http/api"
"github.com/mainflux/mainflux/http/nats"
log "github.com/mainflux/mainflux/logger"
manager "github.com/mainflux/mainflux/manager/client"
broker "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defPort string = "8180"
defNatsURL string = broker.DefaultURL
defManagerURL string = "http://localhost:8180"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envManagerURL string = "MF_MANAGER_URL"
)
type config struct {
ManagerURL string
NatsURL string
Port string
}
func main() {
cfg := config{
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.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()
pub := nats.NewMessagePublisher(nc)
svc := adapter.New(pub)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "http_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "http_adapter",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
mc := manager.NewClient(cfg.ManagerURL)
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
}
-108
View File
@@ -1,108 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/api"
"github.com/mainflux/mainflux/manager/bcrypt"
"github.com/mainflux/mainflux/manager/jwt"
"github.com/mainflux/mainflux/manager/postgres"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defDBHost string = "localhost"
defDBPort string = "5432"
defDBUser string = "mainflux"
defDBPass string = "mainflux"
defDBName string = "manager"
defPort string = "8180"
defSecret string = "manager"
envDBHost string = "MF_DB_HOST"
envDBPort string = "MF_DB_PORT"
envDBUser string = "MF_DB_USER"
envDBPass string = "MF_DB_PASS"
envDBName string = "MF_MANAGER_DB"
envPort string = "MF_MANAGER_PORT"
envSecret string = "MF_MANAGER_SECRET"
)
type config struct {
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
Port string
Secret string
}
func main() {
cfg := config{
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
DBUser: mainflux.Env(envDBUser, defDBUser),
DBPass: mainflux.Env(envDBPass, defDBPass),
DBName: mainflux.Env(envDBName, defDBName),
Port: mainflux.Env(envPort, defPort),
Secret: mainflux.Env(envSecret, defSecret),
}
logger := log.New(os.Stdout)
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
os.Exit(1)
}
defer db.Close()
users := postgres.NewUserRepository(db)
clients := postgres.NewClientRepository(db)
channels := postgres.NewChannelRepository(db)
hasher := bcrypt.New()
idp := jwt.New(cfg.Secret)
svc := manager.New(users, clients, channels, hasher, idp)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "manager",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "manager",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
logger.Info(fmt.Sprintf("Manager service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Manager service terminated: %s", err))
}
-78
View File
@@ -1,78 +0,0 @@
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))
}
-88
View File
@@ -1,88 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
manager "github.com/mainflux/mainflux/manager/client"
adapter "github.com/mainflux/mainflux/ws"
"github.com/mainflux/mainflux/ws/api"
"github.com/mainflux/mainflux/ws/nats"
broker "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defPort = "8180"
defNatsURL = broker.DefaultURL
defManagerURL = "http://localhost:8180"
envPort = "MF_WS_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envManagerURL = "MF_MANAGER_URL"
)
type config struct {
ManagerURL string
NatsURL string
Port string
}
func main() {
cfg := config{
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.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()
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)
mc := manager.NewClient(cfg.ManagerURL)
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc, logger))
}()
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))
}
-52
View File
@@ -1,52 +0,0 @@
# Mainflux CoAP Adapter
Mainflux CoAP adapter provides an [CoAP](http://coap.technology/) API for sending messages through the
platform.
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|-----------------------|-------------------|-----------------------|
| COAP_ADAPTER_NATS_URL | NATS instance URL | nats://localhost:4222 |
## Deployment
The service is distributed as Docker container. The following snippet provides
a compose file template that can be used to deploy the service container locally:
```yaml
version: "2"
services:
adapter:
image: mainflux/coap-adapter:[version]
container_name: [instance name]
ports:
- [host machine port]:5683
environment:
COAP_ADAPTER_NATS_URL: [NATS instance URL]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/coap
# compile the app; make sure to set the proper GOOS value
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# set the environment variables and run the service
COAP_ADAPTER_NATS_URL=[NATS instance URL] app
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
-61
View File
@@ -1,61 +0,0 @@
package adapter
import (
"encoding/json"
"log"
"net"
"github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
const protocol string = "coap"
type Observer struct {
conn *net.UDPConn
addr *net.UDPAddr
message *coap.Message
}
type CoAPAdapter struct {
obsMap map[string][]Observer
logger *zap.Logger
pub mainflux.MessagePublisher
}
// NewCoAPAdapter creates new CoAP adapter struct
func NewCoAPAdapter(logger *zap.Logger, pub mainflux.MessagePublisher) *CoAPAdapter {
ca := &CoAPAdapter{
logger: logger,
pub: pub,
obsMap: make(map[string][]Observer),
}
return ca
}
// Serve function starts CoAP server
func (ca *CoAPAdapter) Serve(addr string) error {
ca.logger.Info("Starting CoAP server", zap.String("address", addr))
return coap.ListenAndServe("udp", addr, ca.COAPServer())
}
// BridgeHandler functions is a handler for messages received via NATS
func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
log.Printf("Received a message: %s\n", string(nm.Data))
// And write it into the database
m := mainflux.RawMessage{}
if len(nm.Data) > 0 {
if err := json.Unmarshal(nm.Data, &m); err != nil {
log.Println("Can not decode adapter msg")
return
}
}
log.Println("Calling obsTransmit()")
log.Println(m.Publisher, m.Protocol, m.Channel, m.Payload)
ca.obsTransmit(m)
}
-145
View File
@@ -1,145 +0,0 @@
package adapter
import (
"bytes"
"log"
"net"
mux "github.com/dereulenspiegel/coap-mux"
coap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
)
func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
log.Printf("Got message in sendMessage: path=%q: %#v from %v", m.Path(), m, a)
var res *coap.Message
if m.IsConfirmable() {
res = &coap.Message{
Type: coap.Acknowledgement,
Code: coap.Content,
MessageID: m.MessageID,
Token: m.Token,
Payload: []byte(""),
}
res.SetOption(coap.ContentFormat, coap.AppJSON)
}
if len(m.Payload) == 0 {
if m.IsConfirmable() {
res.Code = coap.BadRequest
}
return res
}
// Channel ID
cid := mux.Var(m, "channel_id")
n := mainflux.RawMessage{
Channel: cid,
Publisher: "",
Protocol: protocol,
Payload: m.Payload,
}
if err := ca.pub.Publish(n); err != nil {
if m.IsConfirmable() {
res.Code = coap.InternalServerError
}
return res
}
if m.IsConfirmable() {
res.Code = coap.Changed
}
return res
}
func (ca *CoAPAdapter) registerObserver(o Observer, cid string) {
found := false
for _, v := range ca.obsMap[cid] {
if v.addr == o.addr && bytes.Compare(v.message.Token, o.message.Token) == 0 {
found = true
break
}
}
if !found {
log.Println("Register " + cid)
log.Printf("o.message = %v", o.message)
ca.obsMap[cid] = append(ca.obsMap[cid], o)
}
}
func (ca *CoAPAdapter) deregisterObserver(o Observer, cid string) {
for k, v := range ca.obsMap[cid] {
if bytes.Compare(v.message.Token, o.message.Token) == 0 {
// Observer found, remove it from array
log.Println("Deregister " + cid)
ca.obsMap[cid] = append((ca.obsMap[cid])[:k], (ca.obsMap[cid])[k+1:]...)
}
}
}
func (ca *CoAPAdapter) observeMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
log.Printf("Got message in observeMessage: path=%q: %#v from %v", m.Path(), m, a)
var res *coap.Message
if m.IsConfirmable() {
res = &coap.Message{
Type: coap.Acknowledgement,
Code: coap.Content,
MessageID: m.MessageID,
Token: m.Token,
Payload: []byte(""),
}
res.SetOption(coap.ContentFormat, coap.AppJSON)
}
// Channel ID
cid := mux.Var(m, "channel_id")
// Observer
o := Observer{
conn: l,
addr: a,
message: m,
}
if m.Option(coap.Observe) == nil {
if m.IsConfirmable() {
res.Code = coap.BadRequest
}
return res
}
if value, ok := m.Option(coap.Observe).(uint32); ok && value == 0 {
ca.registerObserver(o, cid)
} else {
ca.deregisterObserver(o, cid)
}
if m.IsConfirmable() {
res.Code = coap.Valid
}
return res
}
func (ca *CoAPAdapter) obsTransmit(n mainflux.RawMessage) {
for _, v := range ca.obsMap[n.Channel] {
msg := *(v.message)
msg.Payload = n.Payload
log.Printf("ca.obsMap[cid] = %v", v)
log.Printf("msg = %v", msg)
msg.SetOption(coap.ContentFormat, coap.AppJSON)
msg.SetOption(coap.LocationPath, msg.Path())
log.Printf("Transmitting %v", msg)
err := coap.Transmit(v.conn, v.addr, msg)
if err != nil {
log.Printf("Error on transmitter, stopping: %v", err)
return
}
}
}
-30
View File
@@ -1,30 +0,0 @@
// Package nats contains NATS-specific message repository implementation.
package nats
import (
"github.com/golang/protobuf/proto"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
)
const topic string = "src.coap"
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
type natsPublisher struct {
nc *broker.Conn
}
// NewMessagePublisher instantiates NATS message publisher.
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
return &natsPublisher{nc}
}
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := proto.Marshal(&msg)
if err != nil {
return err
}
return pub.nc.Publish(topic, data)
}
-30
View File
@@ -1,30 +0,0 @@
package adapter
import (
"log"
"net"
"github.com/dereulenspiegel/coap-mux"
"github.com/dustin/go-coap"
)
func notFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
log.Printf("Got message in notFoundHandler: path=%q: %#v from %v", m.Path(), m, a)
if m.IsConfirmable() {
return &coap.Message{
Type: coap.Acknowledgement,
Code: coap.NotFound,
}
}
return nil
}
func (ca *CoAPAdapter) COAPServer() *mux.Router {
r := mux.NewRouter()
r.Handle("/channels/{channel_id}/messages", coap.FuncHandler(ca.sendMessage)).Methods(coap.POST)
r.Handle("/channels/{channel_id}/messages", coap.FuncHandler(ca.observeMessage)).Methods(coap.GET)
r.NotFoundHandler = coap.FuncHandler(notFoundHandler)
return r
}
-3
View File
@@ -1,3 +0,0 @@
// Package mainflux acts as an umbrella package containing multiple different
// microservices and defines all shared domain concepts.
package mainflux
-3
View File
@@ -1,3 +0,0 @@
docker-compose.yml
nginx.conf
ssl/
-14
View File
@@ -1,14 +0,0 @@
FROM golang:1.9-alpine AS builder
ARG SVC_NAME
WORKDIR /go/src/github.com/mainflux/mainflux
COPY . .
RUN apk update \
&& apk add make protobuf git \
&& go get -u github.com/golang/protobuf/protoc-gen-go \
&& make $SVC_NAME \
&& mv build/mainflux-$SVC_NAME /exe
FROM scratch
COPY --from=builder /exe /
ENTRYPOINT ["/exe"]
-95
View File
@@ -1,95 +0,0 @@
###
# Copyright (c) 2015-2017 Mainflux
#
# Mainflux is licensed under an Apache license, version 2.0 license.
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
# See the included LICENSE file for more details.
###
version: "3"
services:
nginx:
image: nginx:1.13-alpine
container_name: mainflux-nginx
restart:
on-failure
volumes:
- $PWD/nginx.conf:/etc/nginx/nginx.conf
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
ports:
- "80:80"
- "443:443"
- "8883:8883"
nats:
image: nats:1.0.2
container_name: mainflux-nats
restart:
on-failure
postgres:
image: postgres:10.2-alpine
container_name: mainflux-postgres
restart:
on-failure
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: mainflux
ports:
- "5432:5432"
manager:
image: mainflux/manager:latest
container_name: mainflux-manager
expose:
- 8180
restart:
on-failure
environment:
MF_DB_HOST: postgres
MF_MANAGER_DB: mainflux
MF_MANAGER_PORT: 8180
MF_MANAGER_SECRET: test-secret
normalizer:
image: mainflux/normalizer:latest
container_name: mainflux-normalizer
restart:
on-failure
expose:
- 8181
environment:
MF_NATS_URL: "nats://nats:4222"
MF_NORMALIZER_PORT: 8181
http-adapter:
image: mainflux/http:latest
container_name: mainflux-http
depends_on:
- manager
restart:
on-failure
expose:
- 8182
environment:
MF_MANAGER_URL: "http://manager:8180"
MF_NATS_URL: "nats://nats:4222"
MF_HTTP_ADAPTER_PORT: 8182
mqtt-adapter:
image: mainflux/mqtt-adapter:latest
container_name: mainflux-mqtt
depends_on:
- manager
restart:
on-failure
environment:
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
AUTH_URL: "http://manager"
AUTH_PORT: 8180
ports:
- "1883:1883"
-187
View File
@@ -1,187 +0,0 @@
###
# Mainflux NGINX Conf
#
# Taken for /etc/nginx/nginx.conf on Debian machine
# and https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/nginx.conf
###
##
# User:
# - 'www-data' on Debian
# - 'nginx' on Alpine
##
#user www-data;
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
###
# HTTP
###
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
upstream docker-manager {
server mainflux-manager:8180;
}
upstream docker-http {
server mainflux-http:8182;
}
##
# Virtual Host Configs
##
# HTTP
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
access_log off;
error_log off;
return 301 https://$server_name$request_uri;
}
# HTTPS
server {
# SSL configuration
#
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
# Certificates
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
ssl_stapling off;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable preloading HSTS for now. You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers "*";
server_name localhost;
# Proxy pass to manager service
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://docker-manager;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location ~ ^/channels/[a-zA-Z0-9-]+/messages$ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://docker-http;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
}
###
# MQTT
###
stream {
upstream docker-mqtt {
server mainflux-mqtt:1883;
}
server {
listen 8883 ssl;
proxy_pass docker-mqtt;
# Certificates
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
}
}
-21
View File
@@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDhDCCAmygAwIBAgIJAPOWQ/k/52/dMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTcwMTA3MDA0MzE4
WhcNMzIwMTA0MDA0MzE4WjBXMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoM
CE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWluZm9AbWFp
bmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzOueTshY
eIDpcedEB+QcgmON72NXVkZ9s9rbNMCCLRoEl947m0SBfz4RXXXRh/plHNgEInsC
Z90LpKhyo5X8U1FIzTNefg3iMdov+wOGnGQJHydLSEbzWCDHjP54+W7XlQaA4/GT
76Os4m6e5LyLdb6jIQQx7EzZnJFQ9g85eldqEGz/tBn6eDLpHSt0ZidClUEGIJcV
5eeGRxKTrExnD1mSkDa63/s6M4fxkacxrwxnwVUdnSvHDxTOt5Mctrqlun63Sn+W
+9Afr5fZUdgROLkLhxWRVUbXMvh3OI5zMHpcONCvIr+gMEeM4q+AyVjCzYzMrDZ7
pvuhM4PGeAMAIwIDAQABo1MwUTAdBgNVHQ4EFgQUiBGMTsVJvNuSKvwoi/7aEvGf
RX0wHwYDVR0jBBgwFoAUiBGMTsVJvNuSKvwoi/7aEvGfRX0wDwYDVR0TAQH/BAUw
AwEB/zANBgkqhkiG9w0BAQ0FAAOCAQEARgi7Cr4JJmoOuP795OYxpVxZBxqnHOMi
2qFeiAwlg3M310YvZaJoewVK44hGNKtRbxc1CelEhRMCFabU5/xQOJmmx3bogNUc
944IfreJyJuUy7q3/6Ix3jf/rH1dpUVZ0S6baldIPKPzNNvmO6VFfmb+dymrMPCM
VQ4w+XoipLS5lPvG4z2/xz2auEAM5qtVjWQFB51Ju63bqKDQTwi+TaQSAu+9Dwr+
10w7qRZogM0anb8Q4Aq14Y7kplnu22VTVHaTyWb5FW4loWKP8FabzNCTOWneidVV
ApEfujCy2Bxw+bNxDtIp1ovPv2vljtJfdnFAKfBElw9BSPg/exUMHw==
-----END CERTIFICATE-----
-30
View File
@@ -1,30 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFFTCCA/2gAwIBAgIJAPtBp1R03oQDMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTcwMTA3MDA1NTIw
WhcNMzIwMTA0MDA1NTIwWjBdMRgwFgYDVQQDDA9tYWluZmx1eC1zZXJ2ZXIxETAP
BgNVBAoMCE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWlu
Zm9AbWFpbmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
mGShbFfHhE/Q/CvVgpfas9o2B2H4E1nrDSmQXqwUPJ2U5+ExqpPkM/xX77yC9gut
k0RrM3fhB5AET9GnGMyFFlNYNPnWZOS6iQN2u5dg02TRqKdIhZZhs438u2wSRVwg
oAqYyiVpoV3QFa2BWTYbWwAhQGPXMUmUN8ZaUcWJ+s8PNhisj458hbWl7HuJ3ICB
xEAXiyd529YVHYJZVHyyEF5GwIvw3DOzZ++Ip6IVd0zYbdHw1pV5SAI5fbc4cb0C
cKXkSlqtjmz6ZeUWLJV98rYHc0YhUR7y2a6zNNFS/ROU6KEsWIdDTNDph0nHS1dB
sj4Abj2Kmf9InIPHHvqF+wIDAQABo4IB3DCCAdgwDAYDVR0TAQH/BAIwADARBglg
hkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgXgMCoGCWCGSAGG+EIBDQQdFhtNYWlu
Zmx1eCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFB7OnWLMd24Eo5dcmJss
lLMkq59vMIGIBgNVHSMEgYAwfoAUiBGMTsVJvNuSKvwoi/7aEvGfRX2hW6RZMFcx
EjAQBgNVBAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsM
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb22CCQDzlkP5P+dv
3TBEBgNVHREEPTA7hwTAqAAuhxD+gAAAAAAAAGJsZv/+yw+7hwR/AAABhxAAAAAA
AAAAAAAAAAAAAAABgglsb2NhbGhvc3QwgYsGA1UdIASBgzCBgDB+BgMrBQgwdzAc
BggrBgEFBQcCARYQaHR0cDovL2xvY2FsaG9zdDBXBggrBgEFBQcCAjBLMA8WCE1h
aW5mbHV4MAMCAQEaOFRoaXMgQ0EgaXMgZm9yIGEgbG9jYWwgTWFpbmZsdXggc2Vy
dmVyIGluc3RhbGxhdGlvbiBvbmx5MA0GCSqGSIb3DQEBDQUAA4IBAQBxGCqJRbnw
lKHhpqZVEEWV1t87wQnf2qOV8SOzh1evF5sYeYEOnb2d802r0p08kuiJNETZdJOh
K/f7dVCL+mtSzHiK8SY8WJ8l0xfW+0qo/GW9jd9QDGbuwi6cRUw1lRhr5p/0ge9N
e3VlI7cjpG/Kv3x1AtCjMpMLzAxOLZmbSWgrMvtJIsMHcQTiV1HexIq9/A3XVthf
zuRUr1qyj3nx6ga2eHqaJQ5/Zu1A7zjHbZTiW4U5Ikl1PDWL3V0uEb3bXZ7xABb9
pjYjDA1Bm4eQMPJ+ZWRs5EFHBnLJc/Kz+4sfUuwiqI4xz2LMeCrdMbh0YSP/rhi4
wERrPpFmvpN3
-----END CERTIFICATE-----
-27
View File
@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmGShbFfHhE/Q/CvVgpfas9o2B2H4E1nrDSmQXqwUPJ2U5+Ex
qpPkM/xX77yC9gutk0RrM3fhB5AET9GnGMyFFlNYNPnWZOS6iQN2u5dg02TRqKdI
hZZhs438u2wSRVwgoAqYyiVpoV3QFa2BWTYbWwAhQGPXMUmUN8ZaUcWJ+s8PNhis
j458hbWl7HuJ3ICBxEAXiyd529YVHYJZVHyyEF5GwIvw3DOzZ++Ip6IVd0zYbdHw
1pV5SAI5fbc4cb0CcKXkSlqtjmz6ZeUWLJV98rYHc0YhUR7y2a6zNNFS/ROU6KEs
WIdDTNDph0nHS1dBsj4Abj2Kmf9InIPHHvqF+wIDAQABAoIBAGyneyyrXXbqDcBu
ZHoLWYTYdaNH57+sYdnto6DMolUhqdS2jFnpvlCOgAhPaTSS2PxiUOjOdWSV+20J
t1EIKW/klsSWyZUAPDuKe7J+2St/+7h7JUsSELEb8HGVOWW4rQ5O3+dpS2ohYEbE
gbAg0tpMOmkVho3+vy4RP76D0MBAnhgl99fjo9jpNxPmBis+L/IJS5SNO1JFXMCf
rvyOIekmtmHBI4PgubVylwOIt03r867gi6WpSOBq5rTusDqwCdmDCeRWDL4EP3Zr
VH8EPbS1Zlcw0FH8yXjp2ts5UcJZL5CdLY7jJ6vkIiw64nNWx7cR4qd55ut280K4
tx4yqnkCgYEAxkQRZgxNHu+7KMUt7gjYRLaNhiSFrcwuSVb8HlhsYYYMH3GPQhvr
dQOOiibzsCna96GLbfdOpi9iwF82uuwNpdeKptPNb5Mht4oNqu5kKg1sl6S6fjSv
yrJACC0NcLRS2LYuBcylUCKsU7g3C9X6VmaSsh415v1s2qFHFik2AycCgYEAxMTt
kHP+pIwr35IvamRCGtUvI0D9R62gxxovpfQeetQMhc/3tOBJe/GOXDP4jitrz1l1
YMcLiCqktFxJqi6UKxcTwnGgcyPyC2UZSJJ/0lOIZDSP7JM7dT1xkrGeQyMjC09C
AwRi/ZAUUwLsMRxfTrj8igF8Md+LIjKBcYdm2w0CgYEAxg6hQsvvDoR09pli9HKp
eJrUbbh2QdPCOUlHuhiizBlYauDKN0QkxlOzRJb8wHJPZyhdXJC8ZI7Zm0qCJeBB
EfZrb5QNmPPlrq+eT66tKMUYQbQxCHohUd8W0BQRZRD94ba76tcwHQlGFKvlcVFk
LoNw77X2KrXm09BgbubkKekCgYAygztZMe3U4AcDRcvWTBaMPN309uIOXIxBkH9a
4uhQL89nKpQ0Yr96ifA5yz2rgYoTmKuBRJe5RPkzM93VSk/PIAV6jSDbbgbc1f8/
mhwmKjuBPd0UpldFKZjWR7KRGZwNczNHAwFGho4xITbxBI+S7fomk2sGgpR9GuoP
8up8oQKBgEMd9Mbo3xC0xW29V5P3FUKH4zRYJYlpk+30bZ4VCrgim9cVBgN/xrIA
l4yEnitEi1591b/r+Uz3b6yWOiLEHRE2U7sQLbfh2fvF0VhFCOE11FeAuoZp89/k
TVnL/FelAMbL1iZFIf9LRY2DdfAUKlO//cXMHcC2iatBUwfylSVS
-----END RSA PRIVATE KEY-----
-8
View File
@@ -1,8 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAquN8NRcSdLOM9RiumqWH8Jw3CGVR/eQQeq+jvT3zpxlUQPAMExQb
MRCspm1oRgDWGvch3Z4zfMmBZyzKJA4BDTh4USzcE5zvnx8aUcUPZPQpwSicKgzb
QGnl0Xf/75GAWrwhxn8GNyMP29wrpcd1Qg8fEQ3HAW1fCd9girKMKY9aBaHli/h2
R9Rd/KTbeqN88aoMjUvZHooIIZXu0A+kyulOajYQO4k3Sp6CBqv0FFcoLQnYNH13
kMUE5qJ68U732HybTw8sofTCOxKcCfM2kVP7dVoF3prlGjUw3z3l3STY8vuTdq0B
R7PslkoQHNmqcL+2gouoWP3GI+IeRzGSSwIBAg==
-----END DH PARAMETERS-----
-1
View File
@@ -1 +0,0 @@
../.github/CONTRIBUTING.md
-1
View File
@@ -1 +0,0 @@
../LICENSE
-47
View File
@@ -1,47 +0,0 @@
## Components
Mainflux IoT platform is comprised of the following services:
| Service | Description |
|:--------------------------------------------------------------------------|:------------------------------------------------------------------------|
| [manager](https://github.com/mainflux/mainflux/tree/master/manager) | Manages platform entities, and auth concerns |
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
| [normalizer](https://github.com/mainflux/mainflux/tree/master/normalizer) | Normalizes SenML messages and generates the "processed" messages stream |
> The following diagram is an (obsolete) overview of platform architecture
![arch](img/architecture.jpg)
## Domain model
The platform is built around 3 main entities: **users**, **clients** and **channels**.
`User` represents the real (human) user of the system. It is represented via its
e-mail and password, which he uses as platform access credentials in order to obtain
an access token. Once logged into the system, user can manage his resources in
CRUD fashion (i.e. channels and clients), and define access control policies
between them.
`Device` is used to represent any device that connects to Mainflux. It is a
generic model that describes any client device of the system.
`Application` is very similar to the `Device` and is represented by the same
`Client` structure (just with different `type` info). Application represents
an end-user application that communicates with devices through Mainflux, and
can be running somewhere in the cloud, locally on the PC or on the mobile phone.
Usually it acquires data from sensor measurement and displays it on various
dashboards.
`Channel` represents a communication channel. It serves as message topic that
can be consumed by all of the clients connected to it.
## Messaging
Mainflux uses [NATS](https://nats.io) as its messaging backbone, due to its
lightweight and performant nature. You can treat its *subjects* as physical
representation of Mainflux channels, where subject name is constructed using
channel unique identifier.
In general, there is no constrained put on content that is being exchanged
through channels. However, in order to be post-processed and normalized,
messages should be formatted using [SenML](https://tools.ietf.org/html/draft-ietf-core-senml-08).
-284
View File
@@ -1,284 +0,0 @@
## Prerequisites
Before proceeding, install the following prerequisites:
- [Docker](https://docs.docker.com/install/)
- [Docker compose](https://docs.docker.com/compose/install/)
- [jsonpp](https://jmhodges.github.io/jsonpp/) (optional)
Once everything is installed, execute the following commands from project root:
```bash
docker-compose -f docker/docker-compose.yml up -d
```
## User management
### Account creation
Use the Mainflux API to create user account:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" https://localhost/users -d '{"email":"john.doe@email.com", "password":"123"}'
```
Note that when using official `docker-compose`, all services are behind `nginx`
proxy and all traffic is `TLS` encrypted.
### Obtaining an authorization key
In order for this user to be able to authenticate to the system, you will have
to create an authorization token for him:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" https://localhost/tokens -d '{"email":"john.doe@email.com", "password":"123"}'
```
Response should look like this:
```
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjMzODg0NzcsImlhdCI6MTUyMzM1MjQ3NywiaXNzIjoibWFpbmZsdXgiLCJzdWIiOiJqb2huLmRvZUBlbWFpbC5jb20ifQ.cygz9zoqD7Rd8f88hpQNilTCAS1DrLLgLg4PRcH-iAI"
}
```
## System provisioning
Before proceeding, make sure that you have created a new account, and obtained
an authorization key.
### Provisioning devices
Devices are provisioned by executing request `POST /clients`, with a
`"type":"device"` specified in JSON payload. Note that you will also need
`user_auth_token` in order to provision clients (both devices and application)
that belong to this particular user.
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"device", "name":"weio"}'
```
Response will contain `Location` header whose value represents path to newly
created client:
```
HTTP/1.1 201 Created
Content-Type: application/json
Location: /clients/81380742-7116-4f6f-9800-14fe464f6773
Date: Tue, 10 Apr 2018 10:02:59 GMT
Content-Length: 0
```
### Provisioning applications
Applications are provisioned by executing HTTP request `POST /clients`, with
`"type":"app"` specified in JSON payload.
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"app", "name":"myapp"}'
```
Response will contain `Location` header whose value represents path to newly
created client (same as for devices):
```
HTTP/1.1 201 Created
Content-Type: application/json
Location: /clients/cb63f852-2d48-44f0-a0cf-e450496c6c92
Date: Tue, 10 Apr 2018 10:33:17 GMT
Content-Length: 0
```
### Retrieving provisioned clients
In order to retrieve data of provisioned clients that is written in database, you
can send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients
```
Notice that you will receive only those clients that were provisioned by
`user_auth_token` owner.
```
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 10 Apr 2018 10:50:12 GMT
Content-Length: 1105
{
"clients": [
{
"id": "81380742-7116-4f6f-9800-14fe464f6773",
"type": "device",
"name": "weio",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTQ1NzksImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiODEzODA3NDItNzExNi00ZjZmLTk4MDAtMTRmZTQ2NGY2NzczIn0.5s8s1hlK-l30kQAyHxEZO_M2NIQw53MQuy7b3Wf3OOE"
},
{
"id": "cb63f852-2d48-44f0-a0cf-e450496c6c92",
"type": "app",
"name": "myapp",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTYzOTcsImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiY2I2M2Y4NTItMmQ0OC00NGYwLWEwY2YtZTQ1MDQ5NmM2YzkyIn0.FE6DWB3yJmBb8uojpQJaKUEbD0Elrjx0HhJA28bVzkU"
}
]
}
```
You can specify `offset` and `limit` parameters in order to fetch specific
group of clients. In that case, your request should look like:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients?offset=0&limit=5
```
If you don't provide them, default values will be used instead: 0 for `offset`,
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
invalid values will be considered malformed request.
### Removing clients
In order to remove you own client you can send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/clients/<client_id>
```
### Provisioning channels
Channels are provisioned by executing request `POST /channels`:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json" -H "Authorization: <user_auth_token>" https://localhost/channels -d '{"name":"mychan"}'
```
After sending request you should receive response with `Location` header that
contains path to newly created channel:
```
HTTP/1.1 201 Created
Content-Type: application/json
Location: /channels/19daa7a8-a489-4571-8714-ef1a214ed914
Date: Tue, 10 Apr 2018 11:30:07 GMT
Content-Length: 0
```
### Retrieving provisioned channels
To retreve provisioned channels you should send request to `/channels` with
authorization token in `Authorization` header:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels
```
Note that you will receive only those channels that were created by authorization
token's owner.
```
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 10 Apr 2018 11:38:06 GMT
Content-Length: 139
{
"channels": [
{
"id": "19daa7a8-a489-4571-8714-ef1a214ed914",
"name": "mychan"
}
]
}
```
You can specify `offset` and `limit` parameters in order to fetch specific
group of channels. In that case, your request should look like:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels?offset=0&limit=5
```
If you don't provide them, default values will be used instead: 0 for `offset`,
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
invalid values will be considered malformed request.
### Removing channels
In order to remove specific channel you should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
```
## Access control
Channel can be observed as a communication group of clients. Only clients that
are connected to the channel can send and receive messages from other clients
in this channel. Clients that are not connected to this channel are not allowed
to communicate over it.
Only user, who is the owner of a channel and of the clients, can connect the
clients to the channel (which is equivalent of giving permissions to these clients
to communicate over given communication group).
To connect client to the channel you should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
```
You can observe which clients are connected to specific channel:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
```
You should receive response with the lists of connected clients in `connected` field
similar to this one:
```
{
"id": "19daa7a8-a489-4571-8714-ef1a214ed914",
"name": "mychan",
"connected": [
{
"id": "81380742-7116-4f6f-9800-14fe464f6773",
"type": "device",
"name": "weio",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTQ1NzksImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiODEzODA3NDItNzExNi00ZjZmLTk4MDAtMTRmZTQ2NGY2NzczIn0.5s8s1hlK-l30kQAyHxEZO_M2NIQw53MQuy7b3Wf3OOE"
}
]
}
```
If you want to disconnect your device from the channel, send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
```
## Sending messages
Once a channel is provisioned and client is connected to it, it can start to
publish messages on the channel. The following sections will provide an example
of message publishing for each of the supported protocols.
### HTTP
To publish message over channel, client should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <client_token>" https://localhost/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
```
Note that you should always send array of messages in senML format.
### WebSocket
To publish and receive messages over channel using web socket, you should first
send handshake request to `/channels/<channel_id>/messages` path. Don't forget
to send `Authorization` header with client authorization token.
If you are not able to send custom headers in your handshake request, send it as
query parameter `authorization`. Then your path should look like this
`/channels/<channel_id>/messages?authorization=<client_auth_key>`.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

-17
View File
@@ -1,17 +0,0 @@
## What is Mainflux?
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.
![banner](img/gopherBanner.jpg)
## 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
-23
View File
@@ -1,23 +0,0 @@
## Test scenarios
Testing environment to be determined.
### Message publishing
In this scenario, large number of requests are sent to HTTP adapter service
every second. This test checks how much time HTTP adapter took to response to
each request.
#### Results
TBD
### Create and get client
In this scenario, large number of requests are sent to manager service to create
client, and than to retrieve its data. This test checks how much time manager
service took to response to each request.
#### Results
TBD
+1433
View File
File diff suppressed because it is too large Load Diff
+1791
View File
File diff suppressed because it is too large Load Diff
+193
View File
@@ -0,0 +1,193 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
openapi: 3.0.1
info:
title: Magistrala http adapter
description: |
HTTP API for sending messages through communication channels.
Some useful links:
- [The Magistrala repository](https://github.com/absmach/magistrala)
contact:
email: info@absmach.eu
license:
name: Apache 2.0
url: https://github.com/absmach/magistrala/blob/main/LICENSE
version: 0.18.0
servers:
- url: http://localhost:8008
- url: https://localhost:8008
tags:
- name: messages
description: Everything about your Messages
externalDocs:
description: Find out more about messages
url: https://magistrala.absmach.eu/docs/
paths:
/m/{domainPrefix}/c/{channelPrefix}:
post:
summary: Sends message to the communication channel
description: |
Sends message to the communication channel. Messages can be sent as
JSON formatted SenML or as blob.
tags:
- messages
parameters:
- $ref: "#/components/parameters/domainPrefix"
- $ref: "#/components/parameters/channelPrefix"
requestBody:
$ref: "#/components/requestBodies/MessageReq"
responses:
"202":
description: Message is accepted for processing.
"400":
description: Message discarded due to its malformed content.
"401":
description: Missing or invalid access token provided.
"403":
description: Access denied to the requested resource.
"404":
description: Message discarded due to invalid channel id.
"415":
description: Message discarded due to invalid or missing content type.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
security: []
responses:
"200":
$ref: "#/components/responses/HealthRes"
"500":
$ref: "#/components/responses/ServiceError"
components:
schemas:
SenMLRecord:
type: object
properties:
bn:
type: string
description: Base Name
bt:
type: number
format: double
description: Base Time
bu:
type: number
format: double
description: Base Unit
bv:
type: number
format: double
description: Base Value
bs:
type: number
format: double
description: Base Sum
bver:
type: number
format: double
description: Version
n:
type: string
description: Name
u:
type: string
description: Unit
v:
type: number
format: double
description: Value
vs:
type: string
description: String Value
vb:
type: boolean
description: Boolean Value
vd:
type: string
description: Data Value
s:
type: number
format: double
description: Value Sum
t:
type: number
format: double
description: Time
ut:
type: number
format: double
description: Update Time
SenMLArray:
type: array
items:
$ref: "#/components/schemas/SenMLRecord"
parameters:
domainPrefix:
name: domainPrefix
description: ID or route of the domain associated with the channel and client.
in: path
schema:
type: string
example: mydomain
required: true
channelPrefix:
name: channelPrefix
description: ID or route of the channel connected to the client.
in: path
schema:
type: string
example: mychannel
required: true
requestBodies:
MessageReq:
description: |
Message to be distributed. Since the platform expects messages to be
properly formatted SenML in order to be post-processed, clients are
obliged to specify Content-Type header for each published message.
Note that all messages that aren't SenML will be accepted and published,
but no post-processing will be applied.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SenMLArray"
responses:
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/health+json:
schema:
$ref: "./schemas/health_info.yaml"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: uuid
description: |
* Client access: "Authorization: Client <client_key>"
basicAuth:
type: http
scheme: basic
description: |
* Clients access: "Authorization: Basic <base64-encoded_credentials>"
security:
- bearerAuth: []
- basicAuth: []
-54
View File
@@ -1,54 +0,0 @@
# HTTP adapter
HTTP adapter provides an HTTP API for sending messages through the platform.
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|----------------------|---------------------|-----------------------|
| MF_MANAGER_URL | Manager service URL | http://localhost:8180 |
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_HTTP_ADAPTER_PORT | Service HTTP port | 8180 |
## Deployment
The service is distributed as Docker container. The following snippet provides
a compose file template that can be used to deploy the service container locally:
```yaml
version: "2"
services:
adapter:
image: mainflux/http:[version]
container_name: [instance name]
ports:
- [host machine port]:8180
environment:
MF_MANAGER_URL: [Manager service URL]
MF_NATS_URL: [NATS instance URL]
MF_HTTP_ADAPTER_PORT: [Service HTTP port]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/http
# compile the app; make sure to set the proper GOOS value
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# set the environment variables and run the service
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] app
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
-18
View File
@@ -1,18 +0,0 @@
package http
import "github.com/mainflux/mainflux"
var _ mainflux.MessagePublisher = (*adapterService)(nil)
type adapterService struct {
pub mainflux.MessagePublisher
}
// New instantiates the domain service implementation.
func New(pub mainflux.MessagePublisher) mainflux.MessagePublisher {
return &adapterService{pub}
}
func (as *adapterService) Publish(msg mainflux.RawMessage) error {
return as.pub.Publish(msg)
}
-3
View File
@@ -1,3 +0,0 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
-16
View File
@@ -1,16 +0,0 @@
package api
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux"
)
func sendMessageEndpoint(svc mainflux.MessagePublisher) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
msg := request.(mainflux.RawMessage)
err := svc.Publish(msg)
return nil, err
}
}
-36
View File
@@ -1,36 +0,0 @@
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
)
var _ mainflux.MessagePublisher = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc mainflux.MessagePublisher
}
// LoggingMiddleware adds logging facilities to the adapter.
func LoggingMiddleware(svc mainflux.MessagePublisher, logger log.Logger) mainflux.MessagePublisher {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method publish took %s to complete", 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)
}
-36
View File
@@ -1,36 +0,0 @@
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux"
)
var _ mainflux.MessagePublisher = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc mainflux.MessagePublisher
}
// MetricsMiddleware instruments adapter by tracking request count and latency.
func MetricsMiddleware(svc mainflux.MessagePublisher, counter metrics.Counter, latency metrics.Histogram) mainflux.MessagePublisher {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
defer func(begin time.Time) {
mm.counter.With("method", "publish").Add(1)
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Publish(msg)
}
-119
View File
@@ -1,119 +0,0 @@
package api
import (
"context"
"errors"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/asaskevich/govalidator"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
manager "github.com/mainflux/mainflux/manager/client"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const protocol string = "http"
var (
errMalformedData error = errors.New("malformed SenML data")
errNotFound error = errors.New("non-existent entity")
auth manager.ManagerClient
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc mainflux.MessagePublisher, mc manager.ManagerClient) http.Handler {
auth = mc
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
r := bone.New()
r.Post("/channels/:id/messages", kithttp.NewServer(
sendMessageEndpoint(svc),
decodeRequest,
encodeResponse,
opts...,
))
r.GetFunc("/version", mainflux.Version("http"))
r.Handle("/metrics", promhttp.Handler())
return r
}
func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
publisher, err := authorize(r)
if err != nil {
return nil, err
}
payload, err := decodePayload(r.Body)
if err != nil {
return nil, err
}
msg := mainflux.RawMessage{
Publisher: publisher,
Protocol: protocol,
ContentType: r.Header.Get("Content-Type"),
Channel: bone.GetValue(r, "id"),
Payload: payload,
}
return msg, nil
}
func authorize(r *http.Request) (string, error) {
apiKey := r.Header.Get("Authorization")
if apiKey == "" {
return "", manager.ErrUnauthorizedAccess
}
// extract ID from /channels/:id/messages
c := strings.Split(r.URL.Path, "/")[2]
if !govalidator.IsUUID(c) {
return "", errNotFound
}
id, err := auth.CanAccess(c, apiKey)
if err != nil {
return "", err
}
return id, nil
}
func decodePayload(body io.ReadCloser) ([]byte, error) {
payload, err := ioutil.ReadAll(body)
if err != nil {
return nil, errMalformedData
}
defer body.Close()
return payload, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.WriteHeader(http.StatusAccepted)
return nil
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
switch err {
case errMalformedData:
w.WriteHeader(http.StatusBadRequest)
case errNotFound:
w.WriteHeader(http.StatusNotFound)
case manager.ErrUnauthorizedAccess:
w.WriteHeader(http.StatusForbidden)
default:
w.WriteHeader(http.StatusInternalServerError)
}
}
-110
View File
@@ -1,110 +0,0 @@
package api_test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mainflux/mainflux"
adapter "github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/http/api"
"github.com/mainflux/mainflux/http/mocks"
manager "github.com/mainflux/mainflux/manager/client"
"github.com/stretchr/testify/assert"
)
const (
id = "123e4567-e89b-12d3-a456-000000000001"
token = "auth_token"
invalidToken = "invalid_token"
msg = `[{"n":"current","t":-1,"v":1.6}]`
)
func newService() mainflux.MessagePublisher {
pub := mocks.NewPublisher()
return adapter.New(pub)
}
func newHTTPServer(pub mainflux.MessagePublisher, mc manager.ManagerClient) *httptest.Server {
mux := api.MakeHandler(pub, mc)
return httptest.NewServer(mux)
}
func newManagerServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "invalid_token" {
w.WriteHeader(http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
}))
}
func newManagerClient(url string) manager.ManagerClient {
return manager.NewClient(url)
}
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 TestPublish(t *testing.T) {
mcServer := newManagerServer()
defer mcServer.Close()
mc := newManagerClient(mcServer.URL)
pub := newService()
ts := newHTTPServer(pub, mc)
defer ts.Close()
client := ts.Client()
cases := map[string]struct {
chanID string
msg string
contentType string
auth string
status int
}{
"publish message": {id, msg, "application/senml+json", token, http.StatusAccepted},
"publish message with no authorization token": {id, msg, "application/senml+json", "", http.StatusForbidden},
"publish message with invalid authorization token": {id, msg, "application/senml+json", invalidToken, http.StatusForbidden},
"publish message with no content type": {id, msg, "", token, http.StatusAccepted},
"publish message with invalid channel id": {"1", msg, "application/senml+json", token, http.StatusNotFound},
}
for desc, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/channels/%s/messages", ts.URL, tc.chanID),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.msg),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", desc, tc.status, res.StatusCode))
}
}
-3
View File
@@ -1,3 +0,0 @@
// Package http contains the domain concept definitions needed to support
// Mainflux http adapter service functionality.
package http
-16
View File
@@ -1,16 +0,0 @@
package mocks
import "github.com/mainflux/mainflux"
var _ (mainflux.MessagePublisher) = (*mockPublisher)(nil)
type mockPublisher struct{}
// NewPublisher returns mock message publisher.
func NewPublisher() mainflux.MessagePublisher {
return mockPublisher{}
}
func (pub mockPublisher) Publish(msg mainflux.RawMessage) error {
return nil
}
-31
View File
@@ -1,31 +0,0 @@
// Package nats contains NATS message publisher implementation.
package nats
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
)
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
type natsPublisher struct {
nc *broker.Conn
}
// NewMessagePublisher instantiates NATS message publisher.
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
return &natsPublisher{nc}
}
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := proto.Marshal(&msg)
if err != nil {
return err
}
subject := fmt.Sprintf("channel.%s", msg.Channel)
return pub.nc.Publish(subject, data)
}
-53
View File
@@ -1,53 +0,0 @@
swagger: "2.0"
info:
title: Mainflux http adapter
description: HTTP API for sending messages through communication channels.
version: "1.0.0"
paths:
/channels/{id}/messages:
post:
summary: Sends message to the communication channel
description: |
Sends message to the communication channel. Messages can be sent as
JSON formatted SenML or as blob.
tags:
- messages
consumes:
- "application/senml+json"
- "text/plain"
produces: []
parameters:
- name: Authorization
description: Access token.
in: header
type: string
required: true
- name: id
description: Unique channel identifier.
in: path
type: string
format: uuid
required: true
- name: message
description: |
Message to be distributed. Since the platform expects messages to be
properly formatted SenML in order to be post-processed, clients are
obliged to specify Content-Type header for each published message.
Note that all messages that aren't SenML will be accepted and published,
but no post-processing will be applied.
in: body
required: true
type: string
responses:
202:
description: Message is accepted for processing.
400:
description: Message discarded due to its malformed content.
403:
description: Message discarded due to missing or invalid credentials.
404:
description: Message discarded due to invalid channel id.
415:
description: Message discarded due to invalid or missing content type.
500:
description: Unexpected server-side error occured.
+181
View File
@@ -0,0 +1,181 @@
<!--
Copyright (c) Abstract Machines
SPDX-License-Identifier: Apache-2.0
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Magistrala API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui.css">
<style>
body {
margin: 0;
padding: 0;
}
.topbar {
display: none;
}
.service-selector {
background: #1b1b1b;
padding: 20px;
text-align: center;
}
.service-selector h1 {
color: #fff;
margin: 0 0 15px 0;
font-family: sans-serif;
}
.service-dropdown-container {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.service-dropdown-container label {
color: #fff;
font-family: sans-serif;
font-size: 14px;
font-weight: 500;
}
.service-dropdown {
background: #2d2d2d;
color: white;
border: 1px solid #4990e2;
padding: 10px 40px 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
min-width: 200px;
appearance: none;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="%23ffffff" d="M6 9L1 4h10z"/></svg>');
background-repeat: no-repeat;
background-position: right 12px center;
}
.service-dropdown:hover {
background-color: #3a3a3a;
border-color: #357abd;
}
.service-dropdown:focus {
outline: none;
border-color: #4990e2;
box-shadow: 0 0 0 2px rgba(73, 144, 226, 0.3);
}
/* Responsive styles for mobile */
@media (max-width: 768px) {
.service-selector {
padding: 15px 10px;
}
.service-selector h1 {
font-size: 1.5rem;
margin: 0 0 12px 0;
}
.service-dropdown-container {
flex-direction: column;
gap: 8px;
}
.service-dropdown-container label {
font-size: 13px;
}
.service-dropdown {
width: 100%;
max-width: 300px;
min-width: auto;
font-size: 13px;
padding: 8px 35px 8px 12px;
}
}
@media (max-width: 480px) {
.service-selector h1 {
font-size: 1.25rem;
}
.service-dropdown {
max-width: 250px;
}
}
</style>
</head>
<body>
<div class="service-selector">
<h1>Magistrala API Documentation</h1>
<div class="service-dropdown-container">
<label for="serviceDropdown">Select Service:</label>
<select id="serviceDropdown" class="service-dropdown"></select>
</div>
</div>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-standalone-preset.js"></script>
<script>
// Available API specifications
const APIs = ["alarms.yaml","auth.yaml","bootstrap.yaml","certs.yaml","channels.yaml","clients.yaml","domains.yaml","groups.yaml","http.yaml","notifiers.yaml","readers.yaml","reports.yaml","rules.yaml","users.yaml"];
// Get the service from URL query parameter, default to first service
function getServiceFromURL() {
const params = new URLSearchParams(window.location.search);
const service = params.get('service');
return service && APIs.includes(service) ? service : APIs[0];
}
// Update URL with selected service
function updateURL(service) {
const url = new URL(window.location);
url.searchParams.set('service', service);
window.history.pushState({}, '', url);
}
// Create service selector dropdown
function createServiceDropdown() {
const dropdown = document.getElementById('serviceDropdown');
const currentService = getServiceFromURL();
APIs.forEach(api => {
const option = document.createElement('option');
option.value = api;
let serviceName = api.replace('.yaml', '').replace(/^\w/, c => c.toUpperCase());
if (serviceName.toLowerCase() === 'http') {
serviceName = 'HTTP';
}
option.textContent = serviceName;
if (api === currentService) {
option.selected = true;
}
dropdown.appendChild(option);
});
// Handle dropdown change
dropdown.addEventListener('change', (e) => {
const selectedApi = e.target.value;
loadSwaggerUI(selectedApi);
updateURL(selectedApi);
});
}
// Load Swagger UI with specified API spec
function loadSwaggerUI(apiSpec) {
SwaggerUIBundle({
url: apiSpec,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
}
// Initialize
createServiceDropdown();
loadSwaggerUI(getServiceFromURL());
</script>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
target/
.classpath
.cache-tests
.cache-main
.settings/
.project
*.class
bin/
-49
View File
@@ -1,49 +0,0 @@
# Load Test
This SBT project contains load tests written for mainflux platform.
## Setup
In order to run load tests you must have [openjdk8](http://openjdk.java.net/install/) and [sbt](https://www.scala-sbt.org/1.0/docs/Setup.html) installed on your machine.
## Configuration
Tests are configured to use variables from `JAVA_OPTS` presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|----------|------------------------------------------|-----------------------|
| manager | Manager service URL | http://localhost:8180 |
| http | HTTP adapter service URL | http://localhost:8182 |
| requests | Number of requests to be sent per second | 100 |
## Usage
This project contains two simulations:
- `PublishSimulation`
- `CreateAndRetrieveClientSimulation`
To run all tests you will have to run following commands:
```bash
cd <path_to_mainflux_project>/load-test
sbt gatling:test
```
### Publish Simulation
`PublishSimulation` contains load tests for publishing messages. To run this test use following command:
```bash
sbt "gatling:testOnly com.mainflux.loadtest.simulations.PublishSimulation"
```
### Create And Retrieve Client Simulation
`CreateAndRetrieveClientSimulation` contains load tests for creating and retrieving clients. To run this test use following command:
```bash
sbt "gatling:testOnly com.mainflux.loadtest.simulations.CreateAndRetrieveClientSimulation"
```
-18
View File
@@ -1,18 +0,0 @@
enablePlugins(GatlingPlugin)
name := "load-test"
version := "0.2.3"
scalaVersion := "2.12.4"
val gatlingVersion = "2.3.1"
val circeVersion = "0.9.3"
libraryDependencies ++= Seq(
"io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion,
"io.gatling" % "gatling-test-framework" % gatlingVersion,
"org.scalaj" %% "scalaj-http" % "2.3.0",
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion
)
-1
View File
@@ -1 +0,0 @@
sbt.version=1.1.2
-1
View File
@@ -1 +0,0 @@
addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.2")
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{15} - %msg%n%rEx</pattern>
</encoder>
<immediateFlush>false</immediateFlush>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="CONSOLE" />
</appender>
<root level="WARN">
<appender-ref ref="ASYNC" />
</root>
</configuration>
@@ -1,7 +0,0 @@
package com.mainflux.loadtest.simulations
object Constants {
val ManagerUrl = System.getProperty("manager", "http://localhost:8180")
val HttpAdapterUrl = System.getProperty("http", "http://localhost:8182")
val RequestsPerSecond = Integer.getInteger("requests", 100)
}
@@ -1,65 +0,0 @@
package com.mainflux.loadtest.simulations
import scala.concurrent.duration._
import scalaj.http.Http
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import CreateAndRetrieveClientSimulation._
import io.gatling.http.protocol.HttpProtocolBuilder.toHttpProtocol
import io.gatling.http.request.builder.HttpRequestBuilder.toActionBuilder
import com.mainflux.loadtest.simulations.Constants._
class CreateAndRetrieveClientSimulation extends Simulation {
// Register user
Http(s"${ManagerUrl}/users")
.postData(User)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
// Login user
val tokenRes = Http(s"${ManagerUrl}/tokens")
.postData(User)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
.body
val tokenCursor = parse(tokenRes).getOrElse(Json.Null).hcursor
val token = tokenCursor.downField("token").as[String].getOrElse("")
// Prepare testing scenario
val httpProtocol = http
.baseURL(ManagerUrl)
.inferHtmlResources()
.acceptHeader("*/*")
.contentTypeHeader(ContentType)
.userAgentHeader("curl/7.54.0")
val scn = scenario("CreateAndGetClient")
.exec(http("CreateClientRequest")
.post("/clients")
.header(HttpHeaderNames.ContentType, ContentType)
.header(HttpHeaderNames.Authorization, token)
.body(StringBody(Client))
.check(status.is(201))
.check(headerRegex(HttpHeaderNames.Location, "(.*)").saveAs("location")))
.exec(http("GetClientRequest")
.get("${location}")
.header(HttpHeaderNames.Authorization, token)
.check(status.is(200)))
setUp(
scn.inject(
constantUsersPerSec(RequestsPerSecond.toDouble) during (15 second))).protocols(httpProtocol)
}
object CreateAndRetrieveClientSimulation {
val ContentType = "application/json"
val User = """{"email":"john.doe@email.com", "password":"123"}"""
val Client = """{"type":"device", "name":"weio"}"""
}
@@ -1,98 +0,0 @@
package com.mainflux.loadtest.simulations
import scala.concurrent.duration._
import scalaj.http.Http
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import PublishSimulation._
import io.gatling.http.protocol.HttpProtocolBuilder.toHttpProtocol
import io.gatling.http.request.builder.HttpRequestBuilder.toActionBuilder
import com.mainflux.loadtest.simulations.Constants._
class PublishSimulation extends Simulation {
// Register user
Http(s"${ManagerUrl}/users")
.postData(User)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
// Login user
val tokenRes = Http(s"${ManagerUrl}/tokens")
.postData(User)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
.body
val tokenCursor = parse(tokenRes).getOrElse(Json.Null).hcursor
val token = tokenCursor.downField("token").as[String].getOrElse("")
// Register client
val clientLocation = Http(s"${ManagerUrl}/clients")
.postData(Client)
.header(HttpHeaderNames.Authorization, token)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
.headers.get("Location").get(0)
val clientId = clientLocation.split("/")(2)
// Get client key
val clientRes = Http(s"${ManagerUrl}/clients/${clientId}")
.header(HttpHeaderNames.Authorization, token)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
.body
val clientCursor = parse(clientRes).getOrElse(Json.Null).hcursor
val clientKey = clientCursor.downField("key").as[String].getOrElse("")
// Register channel
val chanLocation = Http(s"${ManagerUrl}/channels")
.postData(Channel)
.header(HttpHeaderNames.Authorization, token)
.header(HttpHeaderNames.ContentType, ContentType)
.asString
.headers.get("Location").get(0)
val chanId = chanLocation.split("/")(2)
// Connect client to channel
Http(s"${ManagerUrl}/channels/${chanId}/clients/${clientId}")
.method("PUT")
.header(HttpHeaderNames.Authorization, token)
.asString
// Prepare testing scenario
val httpProtocol = http
.baseURL(HttpAdapterUrl)
.inferHtmlResources()
.acceptHeader("*/*")
.contentTypeHeader("application/json; charset=utf-8")
.userAgentHeader("curl/7.54.0")
val scn = scenario("PublishMessage")
.exec(http("PublishMessageRequest")
.post(s"/channels/${chanId}/messages")
.header(HttpHeaderNames.ContentType, "application/senml+json")
.header(HttpHeaderNames.Authorization, clientKey)
.body(StringBody(Message))
.check(status.is(202)))
setUp(
scn.inject(
constantUsersPerSec(RequestsPerSecond.toDouble) during (15 second))).protocols(httpProtocol)
}
object PublishSimulation {
val ContentType = "application/json"
val User = """{"email":"john.doe@email.com", "password":"123"}"""
val Client = """{"type":"device", "name":"weio"}"""
val Channel = """{"name":"mychan"}"""
val Message = """[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]"""
}
-3
View File
@@ -1,3 +0,0 @@
// Package logger contains logger API definition, wrapper that
// can be used around any other logger.
package logger
-23
View File
@@ -1,23 +0,0 @@
package logger
const (
// Error level is used when logging errors.
Error Level = iota + 1
// Warn level is used when logging warnings.
Warn
// Info level is used when logging info data.
Info
)
// Level represents severity level while logging.
type Level int
var levels = map[Level]string{
Error: "error",
Warn: "warn",
Info: "info",
}
func (lvl Level) String() string {
return levels[lvl]
}
-42
View File
@@ -1,42 +0,0 @@
package logger
import (
"io"
"github.com/go-kit/kit/log"
)
// Logger specifies logging API.
type Logger interface {
// Info logs any object in JSON format on info level.
Info(string)
// Warn logs any object in JSON format on warning level.
Warn(string)
// Error logs any object in JSON format on error level.
Error(string)
}
var _ Logger = (*logger)(nil)
type logger struct {
kitLogger log.Logger
}
// New returns wrapped go kit logger.
func New(out io.Writer) Logger {
l := log.NewJSONLogger(log.NewSyncWriter(out))
l = log.With(l, "ts", log.DefaultTimestampUTC)
return &logger{l}
}
func (l logger) Info(msg string) {
l.kitLogger.Log("level", Info.String(), "message", msg)
}
func (l logger) Warn(msg string) {
l.kitLogger.Log("level", Warn.String(), "message", msg)
}
func (l logger) Error(msg string) {
l.kitLogger.Log("level", Error.String(), "message", msg)
}
-93
View File
@@ -1,93 +0,0 @@
package logger_test
import (
"encoding/json"
"fmt"
"io"
"testing"
log "github.com/mainflux/mainflux/logger"
"github.com/stretchr/testify/assert"
)
var _ io.Writer = (*mockWriter)(nil)
type mockWriter struct {
value []byte
}
func (writer *mockWriter) Write(p []byte) (int, error) {
writer.value = p
return len(p), nil
}
func (writer *mockWriter) Read() (logMsg, error) {
var output logMsg
err := json.Unmarshal(writer.value, &output)
return output, err
}
type logMsg struct {
Level string `json:"level"`
Message string `json:"message"`
}
func TestInfo(t *testing.T) {
cases := map[string]struct {
input string
output logMsg
}{
"info log ordinary string": {"input_string", logMsg{log.Info.String(), "input_string"}},
"info log empty string": {"", logMsg{log.Info.String(), ""}},
}
writer := mockWriter{}
logger := log.New(&writer)
for desc, tc := range cases {
logger.Info(tc.input)
output, err := writer.Read()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", desc, tc.output, output))
}
}
func TestWarn(t *testing.T) {
cases := map[string]struct {
input string
output logMsg
}{
"warn log ordinary string": {"input_string", logMsg{log.Warn.String(), "input_string"}},
"warn log empty string": {"", logMsg{log.Warn.String(), ""}},
}
writer := mockWriter{}
logger := log.New(&writer)
for desc, tc := range cases {
logger.Warn(tc.input)
output, err := writer.Read()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", desc, tc.output, output))
}
}
func TestError(t *testing.T) {
cases := map[string]struct {
input string
output logMsg
}{
"error log ordinary string": {"input_string", logMsg{log.Error.String(), "input_string"}},
"error log empty string": {"", logMsg{log.Error.String(), ""}},
}
writer := mockWriter{}
logger := log.New(&writer)
for desc, tc := range cases {
logger.Error(tc.input)
output, err := writer.Read()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", desc, err))
assert.Equal(t, tc.output, output, fmt.Sprintf("%s: expected %s got %s", desc, tc.output, output))
}
}
-75
View File
@@ -1,75 +0,0 @@
# Manager
Manager provides an HTTP API for managing platform resources: users, devices,
applications and channels. Through this API clients are able to do the following
actions:
- register new accounts and obtain access tokens
- provision new clients (i.e. devices & applications)
- create new channels
- "connect" clients into the channels
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
## Configuration
The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|-------------------|------------------------------------------|-----------|
| MF_DB_HOST | Database host address | localhost |
| MF_DB_PORT | Database host port | 5432 |
| MF_DB_USER | Database user | mainflux |
| MF_DB_PASSWORD | Database password | mainflux |
| MF_MANAGER_DB | Name of the database used by the service | manager |
| MF_MANAGER_PORT | Manager service HTTP port | 8180 |
| MF_MANAGER_SECRET | string used for signing tokens | manager |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
services:
manager:
image: mainflux/manager:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_DB_HOST: [Database host address]
MF_DB_PORT: [Database host port]
MF_DB_USER: [Database user]
MF_DB_PASS: [Database password]
MF_MANAGER_DB: [Name of the database used by the service]
MF_MANAGER_PORT: [Service HTTP port]
MF_MANAGER_SECRET: [String used for signing tokens]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/manager
# compile the app; make sure to set the proper GOOS value
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# set the environment variables and run the service
MF_DB_HOST=[Database host address] MF_DB_PORT=[Database host port] MF_DB_USER=[Database user] MF_DB_PASS=[Database password] MF_MANAGER_DB=[Name of the database used by the service] MF_MANAGER_PORT=[Service HTTP port] MF_MANAGER_SECRET=[String used for signing tokens] app
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
-3
View File
@@ -1,3 +0,0 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
-281
View File
@@ -1,281 +0,0 @@
package api
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/manager"
)
func registrationEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(userReq)
if err := req.validate(); err != nil {
return nil, err
}
err := svc.Register(req.user)
return tokenRes{}, err
}
}
func loginEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(userReq)
if err := req.validate(); err != nil {
return nil, err
}
token, err := svc.Login(req.user)
if err != nil {
return nil, err
}
return tokenRes{token}, nil
}
}
func addClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(addClientReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.AddClient(req.key, req.client)
if err != nil {
return nil, err
}
return clientRes{id: id, created: true}, nil
}
}
func updateClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateClientReq)
if err := req.validate(); err != nil {
return nil, err
}
req.client.ID = req.id
if err := svc.UpdateClient(req.key, req.client); err != nil {
return nil, err
}
return clientRes{id: req.id, created: false}, nil
}
}
func viewClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, err
}
client, err := svc.ViewClient(req.key, req.id)
if err != nil {
return nil, err
}
return viewClientRes{client}, nil
}
}
func listClientsEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listResourcesReq)
if err := req.validate(); err != nil {
return nil, err
}
clients, err := svc.ListClients(req.key, req.offset, req.limit)
if err != nil {
return nil, err
}
return listClientsRes{clients}, nil
}
}
func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
err := req.validate()
if err == manager.ErrNotFound {
return removeRes{}, nil
}
if err != nil {
return nil, err
}
if err = svc.RemoveClient(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(createChannelReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.CreateChannel(req.key, req.channel)
if err != nil {
return nil, err
}
return channelRes{id: id, created: true}, nil
}
}
func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateChannelReq)
if err := req.validate(); err != nil {
return nil, err
}
req.channel.ID = req.id
if err := svc.UpdateChannel(req.key, req.channel); err != nil {
return nil, err
}
return channelRes{id: req.id, created: false}, nil
}
}
func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, err
}
channel, err := svc.ViewChannel(req.key, req.id)
if err != nil {
return nil, err
}
return viewChannelRes{channel}, nil
}
}
func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listResourcesReq)
if err := req.validate(); err != nil {
return nil, err
}
channels, err := svc.ListChannels(req.key, req.offset, req.limit)
if err != nil {
return nil, err
}
return listChannelsRes{channels}, nil
}
}
func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
if err == manager.ErrNotFound {
return removeRes{}, nil
}
return nil, err
}
if err := svc.RemoveChannel(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
return nil, err
}
return connectionRes{}, nil
}
}
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Disconnect(cr.key, cr.chanId, cr.clientId); err != nil {
return nil, err
}
return disconnectionRes{}, nil
}
}
func identityEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(identityReq)
if err := req.validate(); err != nil {
return nil, manager.ErrUnauthorizedAccess
}
id, err := svc.Identity(req.key)
if err != nil {
return nil, err
}
return identityRes{id: id}, nil
}
}
func canAccessEndpoint(svc manager.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, manager.ErrUnauthorizedAccess
}
id, err := svc.CanAccess(req.key, req.id)
if err != nil {
return nil, err
}
return identityRes{id: id}, nil
}
}
-772
View File
@@ -1,772 +0,0 @@
package api_test
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/api"
"github.com/mainflux/mainflux/manager/mocks"
"github.com/stretchr/testify/assert"
)
const (
contentType = "application/json"
invalidEmail = "userexample.com"
wrongID = "123e4567-e89b-12d3-a456-000000000042"
id = "123e4567-e89b-12d3-a456-000000000001"
)
var (
user = manager.User{"user@example.com", "password"}
client = manager.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
channel = manager.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() manager.Service {
users := mocks.NewUserRepository()
clients := mocks.NewClientRepository()
channels := mocks.NewChannelRepository(clients)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return manager.New(users, clients, channels, hasher, idp)
}
func newServer(svc manager.Service) *httptest.Server {
mux := api.MakeHandler(svc)
return httptest.NewServer(mux)
}
func toJSON(data interface{}) string {
jsonData, _ := json.Marshal(data)
return string(jsonData)
}
func TestRegister(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
data := toJSON(user)
invalidData := toJSON(manager.User{Email: invalidEmail, Password: "password"})
cases := []struct {
desc string
req string
contentType string
status int
}{
{"register new user", data, contentType, http.StatusCreated},
{"register existing user", data, contentType, http.StatusConflict},
{"register user with invalid email address", invalidData, contentType, http.StatusBadRequest},
{"register user with invalid request format", "{", contentType, http.StatusBadRequest},
{"register user with empty JSON request", "{}", contentType, http.StatusBadRequest},
{"register user with empty request", "", contentType, http.StatusBadRequest},
{"register user with missing content type", data, "", http.StatusUnsupportedMediaType},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/users", ts.URL),
contentType: tc.contentType,
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 TestLogin(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
tokenData := toJSON(map[string]string{"token": user.Email})
data := toJSON(user)
invalidEmailData := toJSON(manager.User{Email: invalidEmail, Password: "password"})
invalidData := toJSON(manager.User{"user@example.com", "invalid_password"})
nonexistentData := toJSON(manager.User{"non-existentuser@example.com", "pass"})
svc.Register(user)
cases := []struct {
desc string
req string
contentType string
status int
res string
}{
{"login with valid credentials", data, contentType, http.StatusCreated, tokenData},
{"login with invalid credentials", invalidData, contentType, http.StatusForbidden, ""},
{"login with invalid email address", invalidEmailData, contentType, http.StatusBadRequest, ""},
{"login non-existent user", nonexistentData, contentType, http.StatusForbidden, ""},
{"login with invalid request format", "{", contentType, http.StatusBadRequest, ""},
{"login with empty JSON request", "{}", contentType, http.StatusBadRequest, ""},
{"login with empty request", "", contentType, http.StatusBadRequest, ""},
{"login with missing content type", data, "", http.StatusUnsupportedMediaType, ""},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/tokens", ts.URL),
contentType: tc.contentType,
body: strings.NewReader(tc.req),
}
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))
token := 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, token, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, token))
}
}
func TestAddClient(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := toJSON(client)
invalidData := toJSON(manager.Client{
Type: "foo",
Name: "invalid_client",
Payload: "some_payload",
})
svc.Register(user)
cases := []struct {
desc string
req string
contentType string
auth string
status int
location string
}{
{"add valid client", data, contentType, user.Email, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
{"add client with invalid data", invalidData, contentType, user.Email, http.StatusBadRequest, ""},
{"add client with invalid auth token", data, contentType, "invalid_token", http.StatusForbidden, ""},
{"add client with invalid request format", "}", contentType, user.Email, http.StatusBadRequest, ""},
{"add client with empty JSON request", "{}", contentType, user.Email, http.StatusBadRequest, ""},
{"add client with empty request", "", contentType, user.Email, http.StatusBadRequest, ""},
{"add client with missing content type", data, "", user.Email, 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()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := toJSON(client)
invalidData := toJSON(manager.Client{
Type: "foo",
Name: client.Name,
Payload: client.Payload,
})
svc.Register(user)
id, _ := svc.AddClient(user.Email, client)
cases := []struct {
desc string
req string
id string
contentType string
auth string
status int
}{
{"update existing client", data, id, contentType, user.Email, http.StatusOK},
{"update non-existent client", data, wrongID, contentType, user.Email, http.StatusNotFound},
{"update client with invalid id", data, "1", contentType, user.Email, http.StatusNotFound},
{"update client with invalid data", invalidData, id, contentType, user.Email, http.StatusBadRequest},
{"update client with invalid user token", data, id, contentType, invalidEmail, http.StatusForbidden},
{"update client with invalid data format", "{", id, contentType, user.Email, http.StatusBadRequest},
{"update client with empty JSON request", "{}", id, contentType, user.Email, http.StatusBadRequest},
{"update client with empty request", "", id, contentType, user.Email, http.StatusBadRequest},
{"update client with missing content type", data, id, "", user.Email, 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),
}
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()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
id, _ := svc.AddClient(user.Email, 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, user.Email, http.StatusOK, data},
{"view non-existent client", wrongID, user.Email, http.StatusNotFound, ""},
{"view client by passing invalid id", "1", user.Email, http.StatusNotFound, ""},
{"view client by passing invalid token", id, invalidEmail, 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) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
noClientsUser := manager.User{Email: "no_clients_user@example.com", Password: user.Password}
svc.Register(noClientsUser)
clients := []manager.Client{}
for i := 0; i < 101; i++ {
id, _ := svc.AddClient(user.Email, client)
client.ID = id
client.Key = id
clients = append(clients, client)
}
clientURL := fmt.Sprintf("%s/clients", ts.URL)
cases := []struct {
desc string
auth string
status int
url string
res []manager.Client
}{
{"get a list of clients", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), clients[0:5]},
{"get a list of clients with invalid token", invalidEmail, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
{"get a list of clients with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
{"get a list of clients with invalid limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
{"get a list of clients with zero limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
{"get a list of clients with no offset provided", user.Email, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), clients[0:5]},
{"get a list of clients with no limit provided", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), clients[1:11]},
{"get a list of clients with redundant query params", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), clients[0:5]},
{"get a list of clients with limit greater than max", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
{"get a list of clients with default URL", user.Email, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), clients[0:10]},
{"get a list of clients with invalid URL", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
{"get a list of clients with invalid number of params", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
{"get a list of clients with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
{"get a list of clients with invalid limit", user.Email, 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][]manager.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()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
id, _ := svc.AddClient(user.Email, client)
cases := []struct {
desc string
id string
auth string
status int
}{
{"delete existing client", id, user.Email, http.StatusNoContent},
{"delete non-existent client", wrongID, user.Email, http.StatusNoContent},
{"delete client with invalid id", "1", user.Email, http.StatusNoContent},
{"delete client with invalid token", id, invalidEmail, 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()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
data := toJSON(channel)
svc.Register(user)
cases := []struct {
desc string
req string
contentType string
auth string
status int
location string
}{
{"create new channel", data, contentType, user.Email, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
{"create new channel with invalid token", data, contentType, invalidEmail, http.StatusForbidden, ""},
{"create new channel with invalid data format", "{", contentType, user.Email, http.StatusBadRequest, ""},
{"create new channel with empty JSON request", "{}", contentType, user.Email, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
{"create new channel with empty request", "", contentType, user.Email, http.StatusBadRequest, ""},
{"create new channel with missing content type", data, "", user.Email, 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()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
updateData := toJSON(map[string]string{
"name": "updated_channel",
})
svc.Register(user)
id, _ := svc.CreateChannel(user.Email, channel)
cases := []struct {
desc string
req string
id string
contentType string
auth string
status int
}{
{"update existing channel", updateData, id, contentType, user.Email, http.StatusOK},
{"update non-existing channel", updateData, wrongID, contentType, user.Email, http.StatusNotFound},
{"update channel with invalid token", updateData, id, contentType, invalidEmail, http.StatusForbidden},
{"update channel with invalid id", updateData, "1", contentType, user.Email, http.StatusNotFound},
{"update channel with invalid data format", "}", id, contentType, user.Email, http.StatusBadRequest},
{"update channel with empty JSON object", "{}", id, contentType, user.Email, http.StatusOK},
{"update channel with empty request", "", id, contentType, user.Email, http.StatusBadRequest},
{"update channel with missing content type", updateData, id, "", user.Email, 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()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
svc.Register(user)
id, _ := svc.CreateChannel(user.Email, channel)
channel.ID = id
data := toJSON(channel)
cases := []struct {
desc string
id string
auth string
status int
res string
}{
{"view existing channel", id, user.Email, http.StatusOK, data},
{"view non-existent channel", wrongID, user.Email, http.StatusNotFound, ""},
{"view channel with invalid id", "1", user.Email, http.StatusNotFound, ""},
{"view channel with invalid token", id, invalidEmail, 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()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
svc.Register(user)
channels := []manager.Channel{}
for i := 0; i < 101; i++ {
id, _ := svc.CreateChannel(user.Email, 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 []manager.Channel
}{
{"get a list of channels", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
{"get a list of channels with invalid token", invalidEmail, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
{"get a list of channels with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
{"get a list of channels with invalid limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
{"get a list of channels with zero limit", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
{"get a list of channels with no offset provided", user.Email, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
{"get a list of channels with no limit provided", user.Email, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
{"get a list of channels with redundant query params", user.Email, 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", user.Email, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
{"get a list of channels with default URL", user.Email, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
{"get a list of channels with invalid URL", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
{"get a list of channels with invalid number of params", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
{"get a list of channels with invalid offset", user.Email, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
{"get a list of channels with invalid limit", user.Email, 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][]manager.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()
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
svc.Register(user)
id, _ := svc.CreateChannel(user.Email, channel)
channel.ID = id
cases := []struct {
desc string
id string
auth string
status int
}{
{"remove existing channel", channel.ID, user.Email, http.StatusNoContent},
{"remove non-existent channel", channel.ID, user.Email, http.StatusNoContent},
{"remove channel with invalid id", wrongID, user.Email, http.StatusNoContent},
{"remove channel with invalid token", channel.ID, invalidEmail, 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) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
clientID, _ := svc.AddClient(user.Email, client)
chanID, _ := svc.CreateChannel(user.Email, channel)
otherUser := manager.User{Email: "other_user@example.com", Password: "password"}
svc.Register(otherUser)
otherClientID, _ := svc.AddClient(otherUser.Email, client)
otherChanID, _ := svc.CreateChannel(otherUser.Email, channel)
cases := []struct {
desc string
chanID string
clientID string
auth string
status int
}{
{"connect existing client to existing channel", chanID, clientID, user.Email, http.StatusOK},
{"connect existing client to non-existent channel", wrongID, clientID, user.Email, http.StatusNotFound},
{"connect client with invalid id to channel", chanID, "1", user.Email, http.StatusNotFound},
{"connect client to channel with invalid id", "1", clientID, user.Email, http.StatusNotFound},
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidEmail, http.StatusForbidden},
{"connect client from owner to channel of other user", otherChanID, clientID, user.Email, http.StatusNotFound},
{"connect client from other user to owner's channel", chanID, otherClientID, user.Email, 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) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
clientID, _ := svc.AddClient(user.Email, client)
chanID, _ := svc.CreateChannel(user.Email, channel)
svc.Connect(user.Email, chanID, clientID)
otherUser := manager.User{Email: "other_user@example.com", Password: "password"}
svc.Register(otherUser)
otherClientID, _ := svc.AddClient(otherUser.Email, client)
otherChanID, _ := svc.CreateChannel(otherUser.Email, channel)
svc.Connect(otherUser.Email, otherChanID, otherClientID)
cases := []struct {
desc string
chanID string
clientID string
auth string
status int
}{
{"disconnect connected client from channel", chanID, clientID, user.Email, http.StatusNoContent},
{"disconnect non-connected client from channel", chanID, clientID, user.Email, http.StatusNotFound},
{"disconnect non-existent client from channel", chanID, "1", user.Email, http.StatusNotFound},
{"disconnect client from non-existent channel", "1", clientID, user.Email, http.StatusNotFound},
{"disconnect client from channel with invalid token", chanID, clientID, invalidEmail, http.StatusForbidden},
{"disconnect owner's client from someone elses channel", otherChanID, clientID, user.Email, http.StatusNotFound},
{"disconnect other's client from owner's channel", chanID, otherClientID, user.Email, 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))
}
}
func TestIdentity(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
clientID, _ := svc.AddClient(user.Email, client)
cases := []struct {
desc string
key string
status int
clientID string
}{
{"get client id using existing client key", clientID, http.StatusOK, clientID},
{"get client id using non-existent client key", "", http.StatusForbidden, ""},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodGet,
url: fmt.Sprintf("%s/access-grant", ts.URL),
token: tc.key,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
clientID := res.Header.Get("X-client-id")
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.clientID, clientID, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.clientID, clientID))
}
}
func TestCanAccess(t *testing.T) {
svc := newService()
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
svc.Register(user)
clientID, _ := svc.AddClient(user.Email, client)
notConnectedClientID, _ := svc.AddClient(user.Email, client)
chanID, _ := svc.CreateChannel(user.Email, channel)
svc.Connect(user.Email, chanID, clientID)
cases := []struct {
desc string
chanID string
clientKey string
status int
clientID string
}{
{"check access to existing channel given connected client", chanID, clientID, http.StatusOK, clientID},
{"check access to existing channel given not connected client", chanID, notConnectedClientID, http.StatusForbidden, ""},
{"check access to existing channel given non-existent client", chanID, "invalid_token", http.StatusForbidden, ""},
{"check access to non-existent channel given existing client", "invalid_token", clientID, http.StatusForbidden, ""},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodGet,
url: fmt.Sprintf("%s/channels/%s/access-grant", ts.URL, tc.chanID),
token: tc.clientKey,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
clientID := res.Header.Get("X-client-id")
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.clientID, clientID, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.clientID, clientID))
}
}
-232
View File
@@ -1,232 +0,0 @@
// +build !test
package api
import (
"fmt"
"time"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/manager"
)
var _ manager.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc manager.Service
}
// LoggingMiddleware adds logging facilities to the core service.
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method register for user %s took %s to complete", user.Email, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Register(user)
}
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method login for user %s took %s to complete", user.Email, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Login(user)
}
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
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 manager.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 manager.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 []manager.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 manager.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 manager.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 manager.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 []manager.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) Identity(key string) (id string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method identity for client %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Identity(key)
}
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method can_access for key %s, channel %s and publisher %s took %s to complete", key, id, pub, time.Since(begin))
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)
}
-172
View File
@@ -1,172 +0,0 @@
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/manager"
)
var _ manager.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc manager.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) Register(user manager.User) error {
defer func(begin time.Time) {
ms.counter.With("method", "register").Add(1)
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Register(user)
}
func (ms *metricsMiddleware) Login(user manager.User) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "login").Add(1)
ms.latency.With("method", "login").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Login(user)
}
func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "add_client").Add(1)
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.AddClient(key, client)
}
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_client").Add(1)
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateClient(key, client)
}
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_client").Add(1)
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewClient(key, id)
}
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]manager.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_clients").Add(1)
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListClients(key, 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 manager.Channel) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_channel").Add(1)
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CreateChannel(key, channel)
}
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_channel").Add(1)
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateChannel(key, channel)
}
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_channel").Add(1)
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewChannel(key, id)
}
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]manager.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_channels").Add(1)
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListChannels(key, 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) Identity(key string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "identity").Add(1)
ms.latency.With("method", "identity").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Identity(key)
}
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "can_access").Add(1)
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CanAccess(key, id)
}
-147
View File
@@ -1,147 +0,0 @@
package api
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/manager"
)
const maxLimitSize = 100
type apiReq interface {
validate() error
}
type userReq struct {
user manager.User
}
func (req userReq) validate() error {
return req.user.Validate()
}
type identityReq struct {
key string
}
func (req identityReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
return nil
}
type addClientReq struct {
key string
client manager.Client
}
func (req addClientReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
return req.client.Validate()
}
type updateClientReq struct {
key string
id string
client manager.Client
}
func (req updateClientReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return manager.ErrNotFound
}
return req.client.Validate()
}
type createChannelReq struct {
key string
channel manager.Channel
}
func (req createChannelReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
return nil
}
type updateChannelReq struct {
key string
id string
channel manager.Channel
}
func (req updateChannelReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return manager.ErrNotFound
}
return nil
}
type viewResourceReq struct {
key string
id string
}
func (req viewResourceReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return manager.ErrNotFound
}
return nil
}
type listResourcesReq struct {
key string
offset int
limit int
}
func (req *listResourcesReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
return nil
}
return manager.ErrMalformedEntity
}
type connectionReq struct {
key string
chanId string
clientId string
}
func (req connectionReq) validate() error {
if req.key == "" {
return manager.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.chanId) || !govalidator.IsUUID(req.clientId) {
return manager.ErrNotFound
}
return nil
}
-204
View File
@@ -1,204 +0,0 @@
package api
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
const wrong string = "?"
var (
client = manager.Client{Type: "app"}
channel = manager.Channel{}
)
func TestUserReqValidation(t *testing.T) {
cases := map[string]struct {
user manager.User
err error
}{
"valid user request": {manager.User{"foo@example.com", "pass"}, nil},
"malformed e-mail": {manager.User{wrong, "pass"}, manager.ErrMalformedEntity},
"empty e-mail": {manager.User{"", "pass"}, manager.ErrMalformedEntity},
"empty password": {manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
}
for desc, tc := range cases {
req := userReq{tc.user}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestIdentityReqValidation(t *testing.T) {
cases := map[string]struct {
key string
err error
}{
"non-empty token": {uuid.NewV4().String(), nil},
"empty token": {"", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := identityReq{tc.key}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
client manager.Client
key string
err error
}{
"valid client addition request": {client, key, nil},
"missing token": {client, "", manager.ErrUnauthorizedAccess},
"wrong client type": {manager.Client{Type: wrong}, key, manager.ErrMalformedEntity},
}
for desc, tc := range cases {
req := addClientReq{
key: tc.key,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
client manager.Client
id string
key string
err error
}{
"valid client update request": {client, id, key, nil},
"non-uuid client ID": {client, wrong, key, manager.ErrNotFound},
"missing token": {client, id, "", manager.ErrUnauthorizedAccess},
"wrong client type": {manager.Client{Type: "invalid"}, id, key, manager.ErrMalformedEntity},
}
for desc, tc := range cases {
req := updateClientReq{
key: tc.key,
id: tc.id,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
"valid channel creation request": {channel, key, nil},
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := createChannelReq{
key: tc.key,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
channel manager.Channel
id string
key string
err error
}{
"valid channel update request": {channel, id, key, nil},
"non-uuid channel ID": {channel, wrong, key, manager.ErrNotFound},
"missing token": {channel, id, "", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := updateChannelReq{
key: tc.key,
id: tc.id,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewResourceReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
id string
key string
err error
}{
"valid resource viewing request": {id, key, nil},
"missing token": {id, "", manager.ErrUnauthorizedAccess},
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
}
for desc, tc := range cases {
req := viewResourceReq{tc.key, tc.id}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListResourcesReqValidation(t *testing.T) {
key := uuid.NewV4().String()
value := 10
cases := map[string]struct {
key string
offset int
limit int
err error
}{
"valid listing request": {key, value, value, nil},
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
"negative offset": {key, -value, value, manager.ErrMalformedEntity},
"zero limit": {key, value, 0, manager.ErrMalformedEntity},
"negative limit": {key, value, -value, manager.ErrMalformedEntity},
"too big limit": {key, value, 20 * value, manager.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))
}
}
-208
View File
@@ -1,208 +0,0 @@
package api
import (
"fmt"
"net/http"
"github.com/mainflux/mainflux/manager"
)
type apiRes interface {
code() int
headers() map[string]string
empty() bool
}
type identityRes struct {
id string
}
func (res identityRes) headers() map[string]string {
return map[string]string{
"X-client-id": res.id,
}
}
func (res identityRes) code() int {
return http.StatusOK
}
func (res identityRes) empty() bool {
return true
}
type tokenRes struct {
Token string `json:"token,omitempty"`
}
func (res tokenRes) code() int {
return http.StatusCreated
}
func (res tokenRes) headers() map[string]string {
return map[string]string{}
}
func (res tokenRes) empty() bool {
return res.Token == ""
}
type removeRes struct{}
func (res removeRes) code() int {
return http.StatusNoContent
}
func (res removeRes) headers() map[string]string {
return map[string]string{}
}
func (res removeRes) empty() bool {
return true
}
type clientRes struct {
id string
created bool
}
func (res clientRes) code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res clientRes) headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprint("/clients/", res.id),
}
}
return map[string]string{}
}
func (res clientRes) empty() bool {
return true
}
type viewClientRes struct {
manager.Client
}
func (res viewClientRes) code() int {
return http.StatusOK
}
func (res viewClientRes) headers() map[string]string {
return map[string]string{}
}
func (res viewClientRes) empty() bool {
return false
}
type listClientsRes struct {
Clients []manager.Client `json:"clients"`
}
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 {
manager.Channel
}
func (res viewChannelRes) code() int {
return http.StatusOK
}
func (res viewChannelRes) headers() map[string]string {
return map[string]string{}
}
func (res viewChannelRes) empty() bool {
return false
}
type listChannelsRes struct {
Channels []manager.Channel `json:"channels"`
}
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
}
-350
View File
@@ -1,350 +0,0 @@
package api
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/manager"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const contentType = "application/json"
var (
errUnsupportedContentType = errors.New("unsupported content type")
errInvalidQueryParams = errors.New("invalid query params")
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc manager.Service) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
r := bone.New()
r.Post("/users", kithttp.NewServer(
registrationEndpoint(svc),
decodeCredentials,
encodeResponse,
opts...,
))
r.Post("/tokens", kithttp.NewServer(
loginEndpoint(svc),
decodeCredentials,
encodeResponse,
opts...,
))
r.Post("/clients", kithttp.NewServer(
addClientEndpoint(svc),
decodeClientCreation,
encodeResponse,
opts...,
))
r.Put("/clients/:id", kithttp.NewServer(
updateClientEndpoint(svc),
decodeClientUpdate,
encodeResponse,
opts...,
))
r.Delete("/clients/:id", kithttp.NewServer(
removeClientEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/clients/:id", kithttp.NewServer(
viewClientEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/clients", kithttp.NewServer(
listClientsEndpoint(svc),
decodeList,
encodeResponse,
opts...,
))
r.Post("/channels", kithttp.NewServer(
createChannelEndpoint(svc),
decodeChannelCreation,
encodeResponse,
opts...,
))
r.Put("/channels/:id", kithttp.NewServer(
updateChannelEndpoint(svc),
decodeChannelUpdate,
encodeResponse,
opts...,
))
r.Delete("/channels/:id", kithttp.NewServer(
removeChannelEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/channels/:id", kithttp.NewServer(
viewChannelEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.Get("/channels", kithttp.NewServer(
listChannelsEndpoint(svc),
decodeList,
encodeResponse,
opts...,
))
r.Put("/channels/:chanId/clients/:clientId", kithttp.NewServer(
connectEndpoint(svc),
decodeConnection,
encodeResponse,
opts...,
))
r.Delete("/channels/:chanId/clients/:clientId", kithttp.NewServer(
disconnectEndpoint(svc),
decodeConnection,
encodeResponse,
opts...,
))
r.Get("/access-grant", kithttp.NewServer(
identityEndpoint(svc),
decodeIdentity,
encodeResponse,
opts...,
))
r.Get("/channels/:id/access-grant", kithttp.NewServer(
canAccessEndpoint(svc),
decodeView,
encodeResponse,
opts...,
))
r.GetFunc("/version", mainflux.Version("manager"))
r.Handle("/metrics", promhttp.Handler())
return r
}
func decodeIdentity(_ context.Context, r *http.Request) (interface{}, error) {
req := identityReq{
key: r.Header.Get("Authorization"),
}
return req, nil
}
func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var user manager.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
return nil, err
}
return userReq{user}, nil
}
func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var client manager.Client
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 manager.Client
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
return nil, err
}
req := updateClientReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
client: client,
}
return req, nil
}
func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var channel manager.Channel
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
return nil, err
}
req := createChannelReq{
key: r.Header.Get("Authorization"),
channel: channel,
}
return req, nil
}
func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error) {
if r.Header.Get("Content-Type") != contentType {
return nil, errUnsupportedContentType
}
var channel manager.Channel
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
return nil, err
}
req := updateChannelReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
channel: channel,
}
return req, nil
}
func decodeView(_ context.Context, r *http.Request) (interface{}, error) {
req := viewResourceReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
}
return req, nil
}
func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
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.(apiRes); ok {
for k, v := range ar.headers() {
w.Header().Set(k, v)
}
w.WriteHeader(ar.code())
if ar.empty() {
return nil
}
}
return json.NewEncoder(w).Encode(response)
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", contentType)
switch err {
case manager.ErrMalformedEntity:
w.WriteHeader(http.StatusBadRequest)
case manager.ErrUnauthorizedAccess:
w.WriteHeader(http.StatusForbidden)
case manager.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
case manager.ErrConflict:
w.WriteHeader(http.StatusConflict)
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)
}
}
}
-2
View File
@@ -1,2 +0,0 @@
// Package bcrypt provides a hasher implementation utilising bcrypt.
package bcrypt
-30
View File
@@ -1,30 +0,0 @@
package bcrypt
import (
"github.com/mainflux/mainflux/manager"
"golang.org/x/crypto/bcrypt"
)
const cost int = 10
var _ manager.Hasher = (*bcryptHasher)(nil)
type bcryptHasher struct{}
// New instantiates a bcrypt-based hasher implementation.
func New() manager.Hasher {
return &bcryptHasher{}
}
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
if err != nil {
return "", err
}
return string(hash), nil
}
func (bh *bcryptHasher) Compare(plain, hashed string) error {
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
}
-44
View File
@@ -1,44 +0,0 @@
package manager
// Channel represents a Mainflux "communication group". This group contains the
// clients that can exchange messages between eachother.
type Channel struct {
ID string `gorm:"type:char(36);primary_key" json:"id"`
Owner string `gorm:"type:varchar(254);not null" json:"-"`
Name string `json:"name,omitempty"`
Clients []Client `gorm:"many2many:channel_clients" json:"connected,omitempty"`
}
// ChannelRepository specifies a channel persistence API.
type ChannelRepository interface {
// Save persists the channel. Successful operation is indicated by unique
// identifier accompanied by nil error response. A non-nil error is
// returned to indicate operation failure.
Save(Channel) (string, error)
// Update performs an update to the existing channel. A non-nil error is
// returned to indicate operation failure.
Update(Channel) error
// One retrieves the channel having the provided identifier, that is owned
// by the specified user.
One(string, string) (Channel, error)
// All retrieves the 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
}
-104
View File
@@ -1,104 +0,0 @@
// Package client provides a manager service client intended for internal
// service communication.
package client
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/mainflux/mainflux/manager"
"github.com/sony/gobreaker"
)
const (
timeout = time.Second * 5
maxFailedReqs = 3
maxFailureRatio = 0.6
)
var (
// ErrServiceUnreachable indicates that the service instance is not available.
ErrServiceUnreachable = errors.New("manager service unavailable")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = manager.ErrUnauthorizedAccess
)
// ManagerClient provides an access to the manager service authorization
// endpoints.
type ManagerClient struct {
url string
cb *gobreaker.CircuitBreaker
}
// NewClient instantiates the manager service client given its base URL.
func NewClient(url string) ManagerClient {
st := gobreaker.Settings{
Name: "Manager",
ReadyToTrip: func(counts gobreaker.Counts) bool {
fr := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= maxFailedReqs && fr >= maxFailureRatio
},
}
mc := ManagerClient{
url: url,
cb: gobreaker.NewCircuitBreaker(st),
}
return mc
}
// VerifyToken tries to extract an identity from the provided token.
func (mc ManagerClient) VerifyToken(token string) (string, error) {
url := fmt.Sprintf("%s/access-grant", mc.url)
return mc.makeRequest(url, token)
}
// CanAccess checks whether or not the client having a provided token has
// access to the specified channel.
func (mc ManagerClient) CanAccess(channel, token string) (string, error) {
url := fmt.Sprintf("%s/channels/%s/access-grant", mc.url, channel)
return mc.makeRequest(url, token)
}
func (mc ManagerClient) makeRequest(url, token string) (string, error) {
response, err := mc.cb.Execute(func() (interface{}, error) {
hc := &http.Client{
Timeout: timeout,
}
mgReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", ErrServiceUnreachable
}
mgReq.Header.Set("Authorization", token)
res, err := hc.Do(mgReq)
if err != nil {
return "", ErrServiceUnreachable
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ErrUnauthorizedAccess, nil
}
return res.Header.Get("X-client-id"), nil
})
if err != nil {
return "", err
}
id, ok := response.(string)
if !ok {
return "", manager.ErrUnauthorizedAccess
}
return id, nil
}
-53
View File
@@ -1,53 +0,0 @@
package manager
import "strings"
// Client represents a Mainflux client. Each client is owned by one user, and
// it is assigned with the unique identifier and (temporary) access key.
type Client struct {
ID string `gorm:"type:char(36);primary_key" json:"id"`
Owner string `gorm:"type:varchar(254);not null" json:"-"`
Type string `gorm:"type:varchar(10);not null" json:"type"`
Name string `json:"name,omitempty"`
Key string `json:"key"`
Payload string `json:"payload,omitempty"`
}
var clientTypes map[string]bool = map[string]bool{
"app": true,
"device": true,
}
// Validate returns an error if client representation is invalid.
func (c *Client) Validate() error {
if c.Type = strings.ToLower(c.Type); !clientTypes[c.Type] {
return ErrMalformedEntity
}
return nil
}
// ClientRepository specifies a client persistence API.
type ClientRepository interface {
// Id generates new resource identifier.
Id() string
// Save persists the client. Successful operation is indicated by non-nil
// error response.
Save(Client) error
// Update performs an update to the existing client. A non-nil error is
// returned to indicate operation failure.
Update(Client) error
// One retrieves the client having the provided identifier, that is owned
// by the specified user.
One(string, string) (Client, error)
// All retrieves the subset of clients owned by the specified user.
All(string, int, int) []Client
// Remove removes the client having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
}
-3
View File
@@ -1,3 +0,0 @@
// Package manager contains the domain concept definitions needed to support
// Mainflux manager service functionality.
package manager
-2
View File
@@ -1,2 +0,0 @@
// Package jwt provides a JWT identity provider.
package jwt
-73
View File
@@ -1,73 +0,0 @@
package jwt
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/mainflux/mainflux/manager"
)
const (
issuer string = "mainflux"
duration time.Duration = 10 * time.Hour
)
var _ manager.IdentityProvider = (*jwtIdentityProvider)(nil)
type jwtIdentityProvider struct {
secret string
}
// New instantiates a JWT identity provider.
func New(secret string) manager.IdentityProvider {
return &jwtIdentityProvider{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, manager.ErrUnauthorizedAccess
}
return []byte(idp.secret), nil
})
if err != nil {
return "", manager.ErrUnauthorizedAccess
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims["sub"].(string), nil
}
return "", manager.ErrUnauthorizedAccess
}
-231
View File
@@ -1,231 +0,0 @@
package manager
var _ Service = (*managerService)(nil)
type managerService struct {
users UserRepository
clients ClientRepository
channels ChannelRepository
hasher Hasher
idp IdentityProvider
}
// New instantiates the domain service implementation.
func New(users UserRepository, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
return &managerService{
users: users,
clients: clients,
channels: channels,
hasher: hasher,
idp: idp,
}
}
func (ms *managerService) Register(user User) error {
hash, err := ms.hasher.Hash(user.Password)
if err != nil {
return ErrMalformedEntity
}
user.Password = hash
return ms.users.Save(user)
}
func (ms *managerService) Login(user User) (string, error) {
dbUser, err := ms.users.One(user.Email)
if err != nil {
return "", ErrUnauthorizedAccess
}
if err := ms.hasher.Compare(user.Password, dbUser.Password); err != nil {
return "", ErrUnauthorizedAccess
}
return ms.idp.TemporaryKey(user.Email)
}
func (ms *managerService) AddClient(key string, client Client) (string, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if _, err := ms.users.One(sub); err != nil {
return "", ErrUnauthorizedAccess
}
client.ID = ms.clients.Id()
client.Owner = sub
client.Key, _ = ms.idp.PermanentKey(client.ID)
return client.ID, ms.clients.Save(client)
}
func (ms *managerService) UpdateClient(key string, client Client) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
client.Owner = sub
return ms.clients.Update(client)
}
func (ms *managerService) ViewClient(key, id string) (Client, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return Client{}, err
}
if _, err := ms.users.One(sub); err != nil {
return Client{}, ErrUnauthorizedAccess
}
return ms.clients.One(sub, id)
}
func (ms *managerService) ListClients(key string, offset, limit int) ([]Client, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return nil, err
}
if _, err := ms.users.One(sub); err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.clients.All(sub, offset, limit), nil
}
func (ms *managerService) RemoveClient(key, id string) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
return ms.clients.Remove(sub, id)
}
func (ms *managerService) CreateChannel(key string, channel Channel) (string, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if _, err := ms.users.One(sub); err != nil {
return "", ErrUnauthorizedAccess
}
channel.Owner = sub
return ms.channels.Save(channel)
}
func (ms *managerService) UpdateChannel(key string, channel Channel) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
channel.Owner = sub
return ms.channels.Update(channel)
}
func (ms *managerService) ViewChannel(key, id string) (Channel, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return Channel{}, err
}
if _, err := ms.users.One(sub); err != nil {
return Channel{}, ErrUnauthorizedAccess
}
return ms.channels.One(sub, id)
}
func (ms *managerService) ListChannels(key string, offset, limit int) ([]Channel, error) {
sub, err := ms.idp.Identity(key)
if err != nil {
return nil, err
}
if _, err := ms.users.One(sub); err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.channels.All(sub, offset, limit), nil
}
func (ms *managerService) RemoveChannel(key, id string) error {
sub, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(sub); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Remove(sub, id)
}
func (ms *managerService) Connect(key, chanId, clientId string) error {
owner, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(owner); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Connect(owner, chanId, clientId)
}
func (ms *managerService) Disconnect(key, chanId, clientId string) error {
owner, err := ms.idp.Identity(key)
if err != nil {
return err
}
if _, err := ms.users.One(owner); err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Disconnect(owner, chanId, clientId)
}
func (ms *managerService) Identity(key string) (string, error) {
client, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
return client, nil
}
func (ms *managerService) CanAccess(key, channel string) (string, error) {
client, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if !ms.channels.HasClient(channel, client) {
return "", ErrUnauthorizedAccess
}
return client, nil
}
-421
View File
@@ -1,421 +0,0 @@
package manager_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/mocks"
"github.com/stretchr/testify/assert"
)
const wrong string = "wrong-value"
var (
user manager.User = manager.User{"user@example.com", "password"}
client manager.Client = manager.Client{Type: "app", Name: "test"}
channel manager.Channel = manager.Channel{Name: "test", Clients: []manager.Client{}}
)
func newService() manager.Service {
users := mocks.NewUserRepository()
clients := mocks.NewClientRepository()
channels := mocks.NewChannelRepository(clients)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return manager.New(users, clients, channels, hasher, idp)
}
func TestRegister(t *testing.T) {
svc := newService()
cases := []struct {
desc string
user manager.User
err error
}{
{"register new user", user, nil},
{"register existing user", user, manager.ErrConflict},
}
for _, tc := range cases {
err := svc.Register(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestLogin(t *testing.T) {
svc := newService()
svc.Register(user)
cases := map[string]struct {
user manager.User
err error
}{
"login with good credentials": {user, nil},
"login with wrong e-mail": {manager.User{wrong, user.Password}, manager.ErrUnauthorizedAccess},
"login with wrong password": {manager.User{user.Email, wrong}, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.Login(tc.user)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClient(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
client manager.Client
key string
err error
}{
"add new app": {manager.Client{Type: "app", Name: "a"}, key, nil},
"add new device": {manager.Client{Type: "device", Name: "b"}, key, nil},
"add client with wrong credentials": {manager.Client{Type: "app", Name: "d"}, wrong, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.AddClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClient(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
client.ID = clientId
cases := map[string]struct {
client manager.Client
key string
err error
}{
"update existing client": {client, key, nil},
"update client with wrong credentials": {client, wrong, manager.ErrUnauthorizedAccess},
"update non-existing client": {manager.Client{ID: "2", Type: "app", Name: "d"}, key, manager.ErrNotFound},
}
for desc, tc := range cases {
err := svc.UpdateClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewClient(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
client.ID = clientId
cases := map[string]struct {
id string
key string
err error
}{
"view existing client": {client.ID, key, nil},
"view client with wrong credentials": {client.ID, wrong, manager.ErrUnauthorizedAccess},
"view non-existing client": {wrong, key, manager.ErrNotFound},
}
for desc, tc := range cases {
_, err := svc.ViewClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListClients(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
n := 10
for i := 0; i < n; i++ {
svc.AddClient(key, client)
}
cases := map[string]struct {
key string
offset int
limit int
size int
err error
}{
"list clients": {key, 0, 5, 5, nil},
"list clients 5-10": {key, 5, 10, 5, nil},
"list last client": {key, 9, 10, 1, nil},
"list empty response": {key, 11, 10, 0, nil},
"list offset < 0": {key, -1, 10, 0, nil},
"list limit < 0": {key, 1, -10, 0, nil},
"list limit = 0": {key, 1, 0, 0, nil},
"list clients with wrong credentials": {wrong, 0, 0, 0, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
client.ID = clientId
cases := map[string]struct {
id string
key string
err error
}{
"remove client with wrong credentials": {client.ID, "?", manager.ErrUnauthorizedAccess},
"remove existing client": {client.ID, key, nil},
"remove removed client": {client.ID, key, nil},
"remove non-existing client": {"?", key, nil},
}
for desc, tc := range cases {
err := svc.RemoveClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannel(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
"create channel": {manager.Channel{}, key, nil},
"create channel with wrong credentials": {manager.Channel{}, wrong, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CreateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannel(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
chanId, _ := svc.CreateChannel(key, channel)
channel.ID = chanId
cases := map[string]struct {
channel manager.Channel
key string
err error
}{
"update existing channel": {channel, key, nil},
"update channel with wrong credentials": {channel, wrong, manager.ErrUnauthorizedAccess},
"update non-existing channel": {manager.Channel{ID: "2", Name: "test"}, key, manager.ErrNotFound},
}
for desc, tc := range cases {
err := svc.UpdateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewChannel(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
chanId, _ := svc.CreateChannel(key, channel)
channel.ID = chanId
cases := map[string]struct {
id string
key string
err error
}{
"view existing channel": {channel.ID, key, nil},
"view channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
"view non-existing channel": {wrong, key, manager.ErrNotFound},
}
for desc, tc := range cases {
_, err := svc.ViewChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListChannels(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
n := 10
for i := 0; i < n; i++ {
svc.CreateChannel(key, channel)
}
cases := map[string]struct {
key string
offset int
limit int
size int
err error
}{
"list first 5 channels": {key, 0, 5, 5, nil},
"list channels 5-10 channels": {key, 5, 10, 5, nil},
"list last channel": {key, 6, 10, 4, nil},
"list offset < 0": {key, -1, 10, 0, nil},
"list limit < 0": {key, 1, -10, 0, nil},
"list limit = 0": {key, 1, 0, 0, nil},
"list channels with wrong credentials": {wrong, 0, 0, 0, manager.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()
svc.Register(user)
key, _ := svc.Login(user)
chanId, _ := svc.CreateChannel(key, channel)
channel.ID = chanId
cases := map[string]struct {
id string
key string
err error
}{
"remove channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
"remove existing channel": {channel.ID, key, nil},
"remove removed channel": {channel.ID, key, nil},
"remove non-existing channel": {channel.ID, key, nil},
}
for desc, tc := range cases {
err := svc.RemoveChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestConnect(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
client.ID = clientId
chanId, _ := svc.CreateChannel(key, channel)
channel.ID = chanId
cases := map[string]struct {
key string
chanId string
clientId string
err error
}{
"connect client": {key, channel.ID, client.ID, nil},
"connect client with wrong credentials": {wrong, channel.ID, client.ID, manager.ErrUnauthorizedAccess},
"connect client to non-existing channel": {key, wrong, client.ID, manager.ErrNotFound},
}
for desc, tc := range cases {
err := svc.Connect(tc.key, tc.chanId, tc.clientId)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestDisconnect(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
client.ID = clientId
chanId, _ := svc.CreateChannel(key, channel)
channel.ID = chanId
svc.Connect(key, chanId, clientId)
cases := []struct {
desc string
key string
chanId string
clientId string
err error
}{
{"disconnect connected client", key, channel.ID, client.ID, nil},
{"disconnect disconnected client", key, channel.ID, client.ID, manager.ErrNotFound},
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, manager.ErrUnauthorizedAccess},
{"disconnect client from non-existing channel", key, wrong, client.ID, manager.ErrNotFound},
{"disconnect non-existing client", key, channel.ID, wrong, manager.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 TestIdentity(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
cases := map[string]struct {
key string
err error
}{
"valid token's identity": {key, nil},
"invalid token's identity": {"", manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.Identity(tc.key)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCanAccess(t *testing.T) {
svc := newService()
svc.Register(user)
key, _ := svc.Login(user)
clientId, _ := svc.AddClient(key, client)
client.ID = clientId
client.Key = clientId
channel.Clients = []manager.Client{client}
chanId, _ := svc.CreateChannel(key, 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": {wrong, channel.ID, manager.ErrUnauthorizedAccess},
"access non-existing channel": {client.Key, wrong, manager.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CanAccess(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
-144
View File
@@ -1,144 +0,0 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/manager"
)
var _ manager.ChannelRepository = (*channelRepositoryMock)(nil)
const chanId = "123e4567-e89b-12d3-a456-"
type channelRepositoryMock struct {
mu sync.Mutex
counter int
channels map[string]manager.Channel
clients manager.ClientRepository
}
// NewChannelRepository creates in-memory channel repository.
func NewChannelRepository(clients manager.ClientRepository) manager.ChannelRepository {
return &channelRepositoryMock{
channels: make(map[string]manager.Channel),
clients: clients,
}
}
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter += 1
channel.ID = fmt.Sprintf("%s%012d", chanId, crm.counter)
crm.channels[key(channel.Owner, channel.ID)] = channel
return channel.ID, nil
}
func (crm *channelRepositoryMock) Update(channel manager.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(channel.Owner, channel.ID)
if _, ok := crm.channels[dbKey]; !ok {
return manager.ErrNotFound
}
crm.channels[dbKey] = channel
return nil
}
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
if c, ok := crm.channels[key(owner, id)]; ok {
return c, nil
}
return manager.Channel{}, manager.ErrNotFound
}
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []manager.Channel {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
channels := make([]manager.Channel, 0)
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 manager.ErrNotFound
}
connected := make([]manager.Client, len(channel.Clients)-1)
for _, client := range channel.Clients {
if client.ID != clientId {
connected = append(connected, client)
}
}
channel.Clients = connected
return crm.Update(channel)
}
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
suffix := fmt.Sprintf("-%s", channel)
for k, v := range crm.channels {
if strings.HasSuffix(k, suffix) {
for _, c := range v.Clients {
if c.ID == client {
return true
}
}
break
}
}
return false
}
-94
View File
@@ -1,94 +0,0 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/manager"
)
var _ manager.ClientRepository = (*clientRepositoryMock)(nil)
const cliId = "123e4567-e89b-12d3-a456-"
type clientRepositoryMock struct {
mu sync.Mutex
counter int
clients map[string]manager.Client
}
// NewClientRepository creates in-memory client repository.
func NewClientRepository() manager.ClientRepository {
return &clientRepositoryMock{
clients: make(map[string]manager.Client),
}
}
func (crm *clientRepositoryMock) Id() string {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter += 1
return fmt.Sprintf("%s%012d", cliId, crm.counter)
}
func (crm *clientRepositoryMock) Save(client manager.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.clients[key(client.Owner, client.ID)] = client
return nil
}
func (crm *clientRepositoryMock) Update(client manager.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(client.Owner, client.ID)
if _, ok := crm.clients[dbKey]; !ok {
return manager.ErrNotFound
}
crm.clients[dbKey] = client
return nil
}
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
if c, ok := crm.clients[key(owner, id)]; ok {
return c, nil
}
return manager.Client{}, manager.ErrNotFound
}
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []manager.Client {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
clients := make([]manager.Client, 0)
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
}
-11
View File
@@ -1,11 +0,0 @@
package mocks
import "fmt"
// Since mocks will store data in map, and they need to resemble the real
// identifiers as much as possible, a key will be created as combination of
// owner and their own identifiers. This will allow searching either by
// prefix or suffix.
func key(owner, id string) string {
return fmt.Sprintf("%s-%s", owner, id)
}
-52
View File
@@ -1,52 +0,0 @@
package mocks
import "github.com/mainflux/mainflux/manager"
var (
_ manager.Hasher = (*hasherMock)(nil)
_ manager.IdentityProvider = (*identityProviderMock)(nil)
)
type hasherMock struct{}
func (hm *hasherMock) Hash(pwd string) (string, error) {
return pwd, nil
}
func (hm *hasherMock) Compare(plain, hashed string) error {
if plain != hashed {
return manager.ErrUnauthorizedAccess
}
return nil
}
type identityProviderMock struct{}
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
if id == "" {
return "", manager.ErrUnauthorizedAccess
}
return id, nil
}
func (idp *identityProviderMock) PermanentKey(id string) (string, error) {
return idp.TemporaryKey(id)
}
func (idp *identityProviderMock) Identity(key string) (string, error) {
return idp.TemporaryKey(key)
}
// NewHasher creates "no-op" hasher for test purposes. This implementation will
// return secrets without changing them.
func NewHasher() manager.Hasher {
return &hasherMock{}
}
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
// token will hold value provided by the caller.
func NewIdentityProvider() manager.IdentityProvider {
return &identityProviderMock{}
}
-44
View File
@@ -1,44 +0,0 @@
package mocks
import (
"sync"
"github.com/mainflux/mainflux/manager"
)
var _ manager.UserRepository = (*userRepositoryMock)(nil)
type userRepositoryMock struct {
mu sync.Mutex
users map[string]manager.User
}
// NewUserRepository creates in-memory user repository.
func NewUserRepository() manager.UserRepository {
return &userRepositoryMock{
users: make(map[string]manager.User),
}
}
func (urm *userRepositoryMock) Save(user manager.User) error {
urm.mu.Lock()
defer urm.mu.Unlock()
if _, ok := urm.users[user.Email]; ok {
return manager.ErrConflict
}
urm.users[user.Email] = user
return nil
}
func (urm *userRepositoryMock) One(email string) (manager.User, error) {
urm.mu.Lock()
defer urm.mu.Unlock()
if val, ok := urm.users[email]; ok {
return val, nil
}
return manager.User{}, manager.ErrUnauthorizedAccess
}

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