mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ada09577e | |||
| 7a747fade7 | |||
| 2d2d23b3ce | |||
| f18e96e990 | |||
| e95e0e4472 | |||
| 4f18a129b3 | |||
| b78b1eb706 | |||
| ee2c0435fc | |||
| 8fde65fee0 | |||
| 02b2facbb0 | |||
| 01125764fb | |||
| 41c9067328 | |||
| 7a6241a80d | |||
| 7b1267e394 | |||
| 2ae21cb38a | |||
| 2380af8a85 | |||
| e289794cfa | |||
| 1a977ff4b1 | |||
| 3a0a958d22 | |||
| 1c0b6623ad | |||
| 461f464329 | |||
| 391203effd | |||
| 86a63dce92 | |||
| d02df10d33 | |||
| 9849b60d68 | |||
| fa2d4c13f4 | |||
| dd9d6f0a88 | |||
| d12b1b164a | |||
| 88f5a60ebb | |||
| 7d6a8998b8 | |||
| bcf6eb5449 | |||
| 0c355c69f0 | |||
| f2eb2f76ef | |||
| e72f7e700f | |||
| 56c5e6acc7 | |||
| 639a945777 | |||
| 16471f4ab1 | |||
| c2731d34b2 | |||
| f9383cd8b0 | |||
| b2e067341c | |||
| cc99241a2b | |||
| c9b214be86 | |||
| 7cef070f47 | |||
| 375d6a83d7 | |||
| 11791c9582 | |||
| 3c2a7c00ac | |||
| 6d1136c0a0 | |||
| 7153e8aa4a | |||
| 7ef8d6f101 | |||
| 5c3d74a7e7 | |||
| a3121a6850 | |||
| ff0fba9914 | |||
| 3159cfc32a | |||
| cc45fe5006 | |||
| d42287f43a | |||
| 96144cf37b | |||
| 456adc3a6a | |||
| 3cffdfe0c4 | |||
| 052db951e3 | |||
| f47ac0753e | |||
| 161ca011ee | |||
| f3bf4af82f | |||
| 53c95f1425 | |||
| 96ea848b4f | |||
| d9cbba5f23 | |||
| 5b99f1f137 | |||
| 6641438d2a | |||
| 3f8e14c593 | |||
| 93086ce204 | |||
| dc41f3a5f1 | |||
| ef58136b85 | |||
| 045517783f | |||
| 98a21ad6d1 | |||
| 450dc6320b | |||
| 5ebd3710e3 | |||
| 44a7e1cc89 | |||
| b2f345f07c | |||
| 58670a8286 | |||
| 854b37724a | |||
| df182e3623 | |||
| fad9f86a7f | |||
| 2a312d8b4b | |||
| d736867bb5 | |||
| bd9dade57d | |||
| 937231fe2d | |||
| 3043d938c1 | |||
| ecf9bb3dc1 | |||
| 0169fe9653 | |||
| 7b640b9d36 | |||
| 714f621be8 | |||
| ad4cfc64ee | |||
| 296a315369 | |||
| 589c6d290b | |||
| 4bd83baad5 | |||
| bbbfc67c66 | |||
| 518495dfc7 | |||
| 0e4f98c0dc | |||
| 79fed471e3 | |||
| 9d41c15920 | |||
| c27a1cb982 | |||
| 321cb1d1dd | |||
| 6a31b3c3bc | |||
| 9ff6dd25c0 | |||
| a5cac6d108 | |||
| 99e1f52c23 | |||
| f4da6959d2 | |||
| e87b5f89ba | |||
| 46ed0cc584 | |||
| 9e297c4a75 | |||
| db09b536bf | |||
| 9e0226eda6 | |||
| 4629441c1f | |||
| 8502263d1f | |||
| c5203886a1 | |||
| 0e0ff18466 | |||
| 3211e79a29 | |||
| f1da6fb601 | |||
| da4471eb63 | |||
| 017ca584aa | |||
| ea9d5857e7 | |||
| 74161fa2fd | |||
| b4edae0070 | |||
| 077d9e53e6 | |||
| 59c1ab659e | |||
| b240a2dc2b | |||
| f859efc984 | |||
| 9280d7b714 | |||
| 8b1188a200 | |||
| 13bf66ca8f | |||
| 64bed5b859 | |||
| 79e3de997f | |||
| ed1b174a84 | |||
| b9aee21eff | |||
| 73240ffedd | |||
| 7f7da3bb80 | |||
| e1fe70766e | |||
| 641ada78da | |||
| 3b76ee386d | |||
| 56d8ff7a7c | |||
| 276531fcf1 | |||
| 2a3de350cc | |||
| 04d614df43 | |||
| 2e49885404 | |||
| 0e0a0b031c |
@@ -1 +0,0 @@
|
||||
* @mainflux/maintainers
|
||||
@@ -1,98 +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
|
||||
following style guides:
|
||||
|
||||
- [Effective Go](https://golang.org/doc/effective_go.html)
|
||||
- [Google's JavaScript styleguide](https://google.github.io/styleguide/jsguide.html)
|
||||
|
||||
Adhering to the following process is the best way to get your work included in the project:
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure
|
||||
the remotes:
|
||||
|
||||
```bash
|
||||
# Clone your fork of the repo into the current directory
|
||||
git clone https://github.com/<your-username>/mainflux.git
|
||||
|
||||
# Navigate to the newly cloned directory
|
||||
cd mainflux
|
||||
|
||||
# Assign the original repo to a remote called "upstream"
|
||||
git remote add upstream https://github.com/mainflux/mainflux.git
|
||||
```
|
||||
|
||||
2. If you cloned a while ago, get the latest changes from upstream:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
```
|
||||
|
||||
3. Create a new topic branch from `master` using the naming convention `mainflux-[issue-num]` to
|
||||
help us keep track of your contribution scope:
|
||||
|
||||
```bash
|
||||
git checkout -b mainflux-[issue-num]
|
||||
```
|
||||
|
||||
4. Commit your changes in logical chunks. When you are ready to commit, make sure to write a Good
|
||||
Commit Message™. Consult the [https://github.com/erlang/otp/wiki/Writing-good-commit-messages](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of working before making them public.
|
||||
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you
|
||||
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions). If you set your `user.name`
|
||||
and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
|
||||
|
||||
5. Locally merge (or rebase) the upstream development branch into your topic branch:
|
||||
|
||||
```bash
|
||||
git pull [--rebase] upstream master
|
||||
```
|
||||
|
||||
6. Push your topic branch up to your fork:
|
||||
|
||||
```bash
|
||||
git push origin mainflux-[issue-num]
|
||||
```
|
||||
|
||||
7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
and detailed description.
|
||||
@@ -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:
|
||||
@@ -1 +0,0 @@
|
||||
build
|
||||
Generated
-391
@@ -1,391 +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 = "73945b6115bfbbcc57d89b7316e28109364124e1"
|
||||
version = "v7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
revision = "61153c768f31ee5f130071d08fc82b85208528de"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
packages = ["."]
|
||||
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/containerd/continuity"
|
||||
packages = ["pathdriver"]
|
||||
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
packages = ["."]
|
||||
revision = "e137e310a99955be01d60b2237da89153ea40987"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
|
||||
version = "v3.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/docker/docker"
|
||||
packages = [
|
||||
"api/types",
|
||||
"api/types/blkiodev",
|
||||
"api/types/container",
|
||||
"api/types/filters",
|
||||
"api/types/mount",
|
||||
"api/types/network",
|
||||
"api/types/registry",
|
||||
"api/types/strslice",
|
||||
"api/types/swarm",
|
||||
"api/types/swarm/runtime",
|
||||
"api/types/versions",
|
||||
"opts",
|
||||
"pkg/archive",
|
||||
"pkg/fileutils",
|
||||
"pkg/homedir",
|
||||
"pkg/idtools",
|
||||
"pkg/ioutils",
|
||||
"pkg/jsonmessage",
|
||||
"pkg/longpath",
|
||||
"pkg/mount",
|
||||
"pkg/pools",
|
||||
"pkg/stdcopy",
|
||||
"pkg/system",
|
||||
"pkg/term",
|
||||
"pkg/term/windows"
|
||||
]
|
||||
revision = "4d9beb4607404e4d756052aca7041517788f7e75"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
packages = ["."]
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsouza/go-dockerclient"
|
||||
packages = ["."]
|
||||
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
"endpoint",
|
||||
"log",
|
||||
"metrics",
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"transport/http"
|
||||
]
|
||||
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
|
||||
version = "v0.6.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-logfmt/logfmt"
|
||||
packages = ["."]
|
||||
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-stack/stack"
|
||||
packages = ["."]
|
||||
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-zoo/bone"
|
||||
packages = ["."]
|
||||
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
|
||||
version = "1.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/gorm"
|
||||
packages = [
|
||||
".",
|
||||
"dialects/postgres"
|
||||
]
|
||||
revision = "58e34726dfc069b558038efbaa25555f182d1f7a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/inflection"
|
||||
packages = ["."]
|
||||
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/logfmt"
|
||||
packages = ["."]
|
||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"hstore",
|
||||
"oid"
|
||||
]
|
||||
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
packages = [
|
||||
".",
|
||||
"encoders/builtin",
|
||||
"util"
|
||||
]
|
||||
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nats-io/nuid"
|
||||
packages = ["."]
|
||||
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/go-digest"
|
||||
packages = ["."]
|
||||
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
|
||||
version = "v1.0.0-rc1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/image-spec"
|
||||
packages = [
|
||||
"specs-go",
|
||||
"specs-go/v1"
|
||||
]
|
||||
revision = "d60099175f88c47cd379c4738d158884749ed235"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/runc"
|
||||
packages = [
|
||||
"libcontainer/system",
|
||||
"libcontainer/user"
|
||||
]
|
||||
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/promhttp"
|
||||
]
|
||||
revision = "c5b7fccd204277076155f10851dad72b76a49317"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "282c8707aa210456a825798969cc27edda34992a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
|
||||
version = "v1.0.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
packages = ["."]
|
||||
revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36"
|
||||
version = "0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "8474b86a5a6f79c443ce4b2992817ff32cf208b8"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"internal/bufferpool",
|
||||
"internal/color",
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "35aad584952c3e7020db7b839f6b102de6271f89"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "88d2dcc510266da9f7f8c7f34e1940716cab5f5c"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
|
||||
version = "v3.1.6"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "9df3ffee2bc61a6ce786baa7570eb61d8275197ea36560cf0cfd7cc0ea53af96"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
[[constraint]]
|
||||
name = "github.com/asaskevich/govalidator"
|
||||
version = "7.0.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
version = "0.6.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-zoo/bone"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/gorm"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
version = "1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.1.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
version = "0.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "go.uber.org/zap"
|
||||
version = "1.7.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
@@ -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 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
@@ -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"
|
||||
@@ -1,48 +0,0 @@
|
||||
BUILD_DIR=build
|
||||
SERVICES=manager http normalizer coap
|
||||
DOCKERS=$(addprefix docker_,$(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
.PHONY: all $(SERVICES) docker
|
||||
|
||||
define compile_service
|
||||
GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -ldflags "-s -w" -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go
|
||||
endef
|
||||
|
||||
define make_docker
|
||||
docker build --build-arg SVC_NAME=$(subst docker_,,$(1)) --tag=mainflux/$(subst docker_,,$(1)) -f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
manager:
|
||||
$(call compile_service,$(@))
|
||||
|
||||
http:
|
||||
$(call compile_service,$(@))
|
||||
|
||||
normalizer:
|
||||
$(call compile_service,$(@))
|
||||
|
||||
coap:
|
||||
$(call compile_service,$(@))
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
# Docker
|
||||
docker_manager:
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker_http:
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker_normalizer:
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker_coap:
|
||||
$(call make_docker,$(@))
|
||||
|
||||
docker: $(DOCKERS)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Mainflux
|
||||
|
||||
[![build][ci-badge]][ci-url]
|
||||
[![go report card][grc-badge]][grc-url]
|
||||
[![license][license]](LICENSE)
|
||||
[![chat][gitter-badge]][gitter]
|
||||
|
||||
![banner][banner]
|
||||
|
||||
Mainflux is modern, scalable, secure open source and patent-free IoT cloud platform written in Go.
|
||||
|
||||
It accepts user, device, and application connections over various network protocols (i.e. HTTP,
|
||||
MQTT, WebSocket, CoAP), thus making a seamless bridge between them. It is used as the IoT middleware
|
||||
for building complex IoT solutions.
|
||||
|
||||
For more details, check out the [official documentation][docs].
|
||||
|
||||
## Features
|
||||
|
||||
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
|
||||
- Device management and provisioning
|
||||
- Fine-grained access control
|
||||
- Platform logging and instrumentation support
|
||||
- Container-based deployment using [Docker][docker]
|
||||
|
||||
## Quickstart
|
||||
|
||||
Before proceeding, install the following prerequisites:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
Once everything is installed, execute the following commands from project root:
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for your interest in Mainflux and wish to contribute!
|
||||
|
||||
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues).
|
||||
2. Checkout the [contribution guide](.github/CONTRIBUTING.md) to learn more about our style and conventions.
|
||||
3. Make your changes compatible to our workflow.
|
||||
|
||||
## Community
|
||||
|
||||
- [Google group][forum]
|
||||
- [Gitter][gitter]
|
||||
- [Twitter][twitter]
|
||||
|
||||
[banner]: https://github.com/mainflux/doc/blob/master/docs/img/gopherBanner.jpg
|
||||
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
|
||||
[ci-url]: https://semaphoreci.com/mainflux/mainflux
|
||||
[docs]: http://mainflux.readthedocs.io
|
||||
[docker]: https://www.docker.com
|
||||
[forum]: https://groups.google.com/forum/#!forum/mainflux
|
||||
[gitter]: https://gitter.im/mainflux/mainflux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
|
||||
[grc-badge]: https://goreportcard.com/badge/github.com/mainflux/mainflux
|
||||
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
+508
@@ -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>"
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+722
@@ -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
File diff suppressed because it is too large
Load Diff
+1585
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/nats"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort string = "8180"
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defManagerURL string = "http://localhost:8180"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envManagerURL string = "MF_MANAGER_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ManagerURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Log("error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
|
||||
svc := adapter.New(pub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "http_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "http_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
logger.Log("terminated", <-errs)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/bcrypt"
|
||||
"github.com/mainflux/mainflux/manager/jwt"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost string = "localhost"
|
||||
defDBPort string = "5432"
|
||||
defDBUser string = "mainflux"
|
||||
defDBPass string = "mainflux"
|
||||
defDBName string = "manager"
|
||||
defPort string = "8180"
|
||||
defSecret string = "manager"
|
||||
envDBHost string = "MF_DB_HOST"
|
||||
envDBPort string = "MF_DB_PORT"
|
||||
envDBUser string = "MF_DB_USER"
|
||||
envDBPass string = "MF_DB_PASS"
|
||||
envDBName string = "MF_MANAGER_DB"
|
||||
envPort string = "MF_MANAGER_PORT"
|
||||
envSecret string = "MF_MANAGER_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
Port string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Log("error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
users := postgres.NewUserRepository(db)
|
||||
clients := postgres.NewClientRepository(db)
|
||||
channels := postgres.NewChannelRepository(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(cfg.Secret)
|
||||
|
||||
svc := manager.New(users, clients, channels, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
logger.Log("terminated", <-errs)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/normalizer"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
defNatsURL string = nats.DefaultURL
|
||||
defPort string = "8180"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envPort string = "MF_NORMALIZER_PORT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Log("error", fmt.Sprintf("Failed to connect: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
normalizer.Subscribe(nc, logger)
|
||||
logger.Log("terminated", <-errs)
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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 recieved via NATS
|
||||
func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
|
||||
log.Printf("Received a message: %s\n", string(nm.Data))
|
||||
|
||||
// And write it into the database
|
||||
m := mainflux.RawMessage{}
|
||||
if len(nm.Data) > 0 {
|
||||
if err := json.Unmarshal(nm.Data, &m); err != nil {
|
||||
log.Println("Can not decode adapter msg")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Calling obsTransmit()")
|
||||
log.Println(m.Publisher, m.Protocol, m.Channel, m.Payload)
|
||||
ca.obsTransmit(m)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Package nats contains NATS-specific message repository implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "src.coap"
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// NewMessagePublisher instantiates NATS message publisher.
|
||||
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
return &natsPublisher{nc}
|
||||
}
|
||||
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pub.nc.Publish(topic, data)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package mainflux acts as an umbrella package containing multiple different
|
||||
// microservices and defines all shared domain concepts.
|
||||
package mainflux
|
||||
@@ -1,3 +0,0 @@
|
||||
docker-compose.yml
|
||||
nginx.conf
|
||||
ssl/
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM golang:1.9-alpine AS builder
|
||||
ARG SVC_NAME
|
||||
RUN apk update \
|
||||
&& apk add git \
|
||||
&& go get github.com/mainflux/mainflux \
|
||||
&& cd /go/src/github.com/mainflux/mainflux/cmd/$SVC_NAME \
|
||||
&& CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o /exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /exe /
|
||||
ENTRYPOINT ["/exe"]
|
||||
@@ -1,78 +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
|
||||
network_mode: bridge
|
||||
volumes:
|
||||
- $PWD/nginx.conf:/etc/nginx/nginx.conf
|
||||
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
|
||||
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
|
||||
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8883:8883"
|
||||
|
||||
nats:
|
||||
image: nats:1.0.2
|
||||
container_name: mainflux-nats
|
||||
network_mode: bridge
|
||||
|
||||
postgres:
|
||||
image: postgres:10.2-alpine
|
||||
container_name: mainflux-postgres
|
||||
network_mode: bridge
|
||||
environment:
|
||||
POSTGRES_USER: mainflux
|
||||
POSTGRES_PASSWORD: mainflux
|
||||
POSTGRES_DB: mainflux
|
||||
|
||||
manager:
|
||||
image: mainflux/manager:latest
|
||||
container_name: mainflux-manager
|
||||
network_mode: bridge
|
||||
environment:
|
||||
MF_DB_HOST: postgres
|
||||
MF_MANAGER_DB: mainflux
|
||||
MF_MANAGER_PORT: 8180
|
||||
MF_MANAGER_SECRET: test-secret
|
||||
|
||||
normalizer:
|
||||
image: mainflux/normalizer:latest
|
||||
container_name: mainflux-normalizer
|
||||
network_mode: bridge
|
||||
environment:
|
||||
MF_NATS_URL: "nats://nats:4222"
|
||||
MF_NORMALIZER_PORT: 8181
|
||||
|
||||
http-adapter:
|
||||
image: mainflux/http:latest
|
||||
container_name: mainflux-http
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
environment:
|
||||
MF_MANAGER_URL: "http://manager:8180"
|
||||
MF_NATS_URL: "nats://nats:4222"
|
||||
MF_HTTP_ADAPTER_PORT: 8182
|
||||
|
||||
mqtt-adapter:
|
||||
image: mainflux/mqtt-adapter:latest
|
||||
container_name: mainflux-mqtt
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
environment:
|
||||
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
|
||||
AUTH_URL: "http://manager"
|
||||
AUTH_PORT: 8180
|
||||
@@ -1,186 +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;
|
||||
return 302 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS
|
||||
server {
|
||||
# SSL configuration
|
||||
#
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
|
||||
#
|
||||
# Note: You should disable gzip for SSL traffic.
|
||||
# See: https://bugs.debian.org/773332
|
||||
#
|
||||
# Read up on ssl_ciphers to ensure a secure configuration.
|
||||
# See: https://bugs.debian.org/765782
|
||||
#
|
||||
# Self signed certs generated by the ssl-cert package
|
||||
# Don't use them in a production server!
|
||||
#
|
||||
# include snippets/snakeoil.conf;
|
||||
|
||||
# Certificates
|
||||
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
|
||||
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
|
||||
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
|
||||
|
||||
# from https://cipherli.st/
|
||||
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling off;
|
||||
ssl_stapling_verify on;
|
||||
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
||||
resolver_timeout 5s;
|
||||
|
||||
# Disable preloading HSTS for now. You can use the commented out header line that includes
|
||||
# the "preload" directive if you understand the implications.
|
||||
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin '*';
|
||||
add_header Access-Control-Allow-Methods '*';
|
||||
add_header Access-Control-Allow-Headers "*";
|
||||
|
||||
|
||||
server_name localhost;
|
||||
|
||||
# Proxy pass to manager service
|
||||
location / {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://docker-manager/;
|
||||
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to mainflux-http-adapter
|
||||
location ~ ^/channels/[a-zA-Z0-9-]+/messages$ {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://docker-http/;
|
||||
# Allow OPTIONS method CORS
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
###
|
||||
# MQTT
|
||||
###
|
||||
stream {
|
||||
|
||||
upstream docker-mqtt {
|
||||
server mainflux-mqtt:1883;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8883 ssl;
|
||||
proxy_pass docker-mqtt;
|
||||
# Certificates
|
||||
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
|
||||
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
|
||||
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
}
|
||||
}
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN DH PARAMETERS-----
|
||||
MIIBCAKCAQEAquN8NRcSdLOM9RiumqWH8Jw3CGVR/eQQeq+jvT3zpxlUQPAMExQb
|
||||
MRCspm1oRgDWGvch3Z4zfMmBZyzKJA4BDTh4USzcE5zvnx8aUcUPZPQpwSicKgzb
|
||||
QGnl0Xf/75GAWrwhxn8GNyMP29wrpcd1Qg8fEQ3HAW1fCd9girKMKY9aBaHli/h2
|
||||
R9Rd/KTbeqN88aoMjUvZHooIIZXu0A+kyulOajYQO4k3Sp6CBqv0FFcoLQnYNH13
|
||||
kMUE5qJ68U732HybTw8sofTCOxKcCfM2kVP7dVoF3prlGjUw3z3l3STY8vuTdq0B
|
||||
R7PslkoQHNmqcL+2gouoWP3GI+IeRzGSSwIBAg==
|
||||
-----END DH PARAMETERS-----
|
||||
+1433
File diff suppressed because it is too large
Load Diff
+1791
File diff suppressed because it is too large
Load Diff
@@ -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: []
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var _ mainflux.MessagePublisher = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc mainflux.MessagePublisher
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the adapter.
|
||||
func LoggingMiddleware(svc mainflux.MessagePublisher, logger log.Logger) mainflux.MessagePublisher {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "publish",
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Publish(msg)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const protocol string = "http"
|
||||
|
||||
var (
|
||||
errMalformedData error = errors.New("malformed SenML data")
|
||||
errUnknownType error = errors.New("unknown content type")
|
||||
errUnauthorizedAccess error = errors.New("missing or invalid credentials provided")
|
||||
auth manager.ManagerClient
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc mainflux.MessagePublisher, mc manager.ManagerClient) http.Handler {
|
||||
auth = mc
|
||||
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/channels/:id/messages", kithttp.NewServer(
|
||||
sendMessageEndpoint(svc),
|
||||
decodeRequest,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version())
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
publisher, err := authorize(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload, err := decodePayload(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := mainflux.RawMessage{
|
||||
Publisher: publisher,
|
||||
Protocol: protocol,
|
||||
ContentType: r.Header.Get("Content-Type"),
|
||||
Channel: bone.GetValue(r, "id"),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func authorize(r *http.Request) (string, error) {
|
||||
apiKey := r.Header.Get("Authorization")
|
||||
|
||||
if apiKey == "" {
|
||||
return "", errUnauthorizedAccess
|
||||
}
|
||||
|
||||
// extract ID from /channels/:id/messages
|
||||
c := strings.Split(r.URL.Path, "/")[2]
|
||||
|
||||
id, err := auth.CanAccess(c, apiKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func decodePayload(body io.ReadCloser) ([]byte, error) {
|
||||
payload, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, errMalformedData
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch err {
|
||||
case errMalformedData:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errUnknownType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case manager.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package http contains the domain concept definitions needed to support
|
||||
// Mainflux http adapter service functionality.
|
||||
package http
|
||||
@@ -1,31 +0,0 @@
|
||||
// Package nats contains NATS message publisher implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "src.http"
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// NewMessagePublisher instantiates NATS message publisher.
|
||||
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
return &natsPublisher{nc}
|
||||
}
|
||||
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pub.nc.Publish(topic, data)
|
||||
}
|
||||
@@ -1,51 +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.
|
||||
415:
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
500:
|
||||
description: Unexpected server-side error occured.
|
||||
+181
@@ -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>
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listClientsRes{clients, len(clients)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeClientEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
err := req.validate()
|
||||
if err == manager.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = svc.RemoveClient(req.key, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func createChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createChannelReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := svc.CreateChannel(req.key, req.channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channelRes{id: id, created: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateChannelReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.channel.ID = req.id
|
||||
|
||||
if err := svc.UpdateChannel(req.key, req.channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channelRes{id: req.id, created: false}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channel, err := svc.ViewChannel(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return viewChannelRes{channel}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listChannelsEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listResourcesReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels, err := svc.ListChannels(req.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return listChannelsRes{channels, len(channels)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
if err == manager.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.RemoveChannel(req.key, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
if err := cr.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
if err := cr.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identityEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(identityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, err := svc.Identity(req.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func canAccessEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
id, err := svc.CanAccess(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identityRes{id: id}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc manager.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "register",
|
||||
"email", user.Email,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Register(user)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "login",
|
||||
"email", user.Email,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Login(user)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "add_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "update_client",
|
||||
"key", key,
|
||||
"id", client.ID,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "view_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListClients(key string) (clients []manager.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "list_clients",
|
||||
"key", key,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListClients(key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "remove_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "create_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "update_channel",
|
||||
"key", key,
|
||||
"id", channel.ID,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "view_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannels(key string) (channels []manager.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "list_channels",
|
||||
"key", key,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChannels(key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "remove_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Connect(key, chanId, clientId string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "connect",
|
||||
"key", key,
|
||||
"channel", chanId,
|
||||
"client", clientId,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Connect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Disconnect(key, chanId, clientId string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "disconnect",
|
||||
"key", key,
|
||||
"channel", chanId,
|
||||
"client", clientId,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Disconnect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "identity",
|
||||
"id", id,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Identity(key)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "can_access",
|
||||
"key", key,
|
||||
"id", id,
|
||||
"publisher", pub,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.CanAccess(key, id)
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc manager.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Register(user manager.User) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "register").Add(1)
|
||||
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Register(user)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Login(user manager.User) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "login").Add(1)
|
||||
ms.latency.With("method", "login").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Login(user)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_client").Add(1)
|
||||
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_client").Add(1)
|
||||
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_client").Add(1)
|
||||
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListClients(key string) ([]manager.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_clients").Add(1)
|
||||
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListClients(key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_client").Add(1)
|
||||
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_channel").Add(1)
|
||||
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_channel").Add(1)
|
||||
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_channel").Add(1)
|
||||
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannels(key string) ([]manager.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_channels").Add(1)
|
||||
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChannels(key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_channel").Add(1)
|
||||
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Connect(key, chanId, clientId string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "connect").Add(1)
|
||||
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Connect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Disconnect(key, chanId, clientId string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "disconnect").Add(1)
|
||||
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Disconnect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identity(key string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identity").Add(1)
|
||||
ms.latency.With("method", "identity").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Identity(key)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "can_access").Add(1)
|
||||
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.CanAccess(key, id)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type userReq struct {
|
||||
user manager.User
|
||||
}
|
||||
|
||||
func (req userReq) validate() error {
|
||||
return req.user.Validate()
|
||||
}
|
||||
|
||||
type identityReq struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (req identityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type addClientReq struct {
|
||||
key string
|
||||
client manager.Client
|
||||
}
|
||||
|
||||
func (req addClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
}
|
||||
|
||||
type updateClientReq struct {
|
||||
key string
|
||||
id string
|
||||
client manager.Client
|
||||
}
|
||||
|
||||
func (req updateClientReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return req.client.Validate()
|
||||
}
|
||||
|
||||
type createChannelReq struct {
|
||||
key string
|
||||
channel manager.Channel
|
||||
}
|
||||
|
||||
func (req createChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateChannelReq struct {
|
||||
key string
|
||||
id string
|
||||
channel manager.Channel
|
||||
}
|
||||
|
||||
func (req updateChannelReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewResourceReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req viewResourceReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.id) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listResourcesReq struct {
|
||||
key string
|
||||
size int
|
||||
offset int
|
||||
}
|
||||
|
||||
func (req listResourcesReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.size > 0 && req.offset >= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.ErrMalformedEntity
|
||||
}
|
||||
|
||||
type connectionReq struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
}
|
||||
|
||||
func (req connectionReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanId) && !govalidator.IsUUID(req.clientId) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,203 +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 = manager.Client{Type: "app"}
|
||||
channel manager.Channel = manager.Channel{}
|
||||
)
|
||||
|
||||
func TestUserReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"valid user request": {manager.User{"foo@example.com", "pass"}, nil},
|
||||
"malformed e-mail": {manager.User{wrong, "pass"}, manager.ErrMalformedEntity},
|
||||
"empty e-mail": {manager.User{"", "pass"}, manager.ErrMalformedEntity},
|
||||
"empty password": {manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := userReq{tc.user}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityReqValidation(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"non-empty token": {uuid.NewV4().String(), nil},
|
||||
"empty token": {"", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := identityReq{tc.key}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client addition request": {client, key, nil},
|
||||
"missing token": {client, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: wrong}, key, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := addClientReq{
|
||||
key: tc.key,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClientReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid client update request": {client, id, key, nil},
|
||||
"non-uuid client ID": {client, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {client, id, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: "invalid"}, id, key, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := updateClientReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel creation request": {channel, key, nil},
|
||||
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := createChannelReq{
|
||||
key: tc.key,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid channel update request": {channel, id, key, nil},
|
||||
"non-uuid channel ID": {channel, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {channel, id, "", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := updateChannelReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewResourceReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid resource viewing request": {id, key, nil},
|
||||
"missing token": {id, "", manager.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := viewResourceReq{tc.key, tc.id}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListResourcesReqValidation(t *testing.T) {
|
||||
key := uuid.NewV4().String()
|
||||
value := 10
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
size int
|
||||
offset int
|
||||
err error
|
||||
}{
|
||||
"valid listing request": {key, value, value, nil},
|
||||
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
|
||||
"negative offset": {key, value, -value, manager.ErrMalformedEntity},
|
||||
"zero size": {key, 0, value, manager.ErrMalformedEntity},
|
||||
"negative size": {key, -value, value, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
req := listResourcesReq{
|
||||
key: tc.key,
|
||||
size: tc.size,
|
||||
offset: tc.offset,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
const contentType = "application/json; charset=utf-8"
|
||||
|
||||
type apiRes interface {
|
||||
code() int
|
||||
headers() map[string]string
|
||||
empty() bool
|
||||
}
|
||||
|
||||
type identityRes struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (res identityRes) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-client-id": res.id,
|
||||
}
|
||||
}
|
||||
|
||||
func (res identityRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res identityRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type tokenRes struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (res tokenRes) code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res tokenRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res tokenRes) empty() bool {
|
||||
return res.Token == ""
|
||||
}
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type clientRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res clientRes) code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res clientRes) headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/clients/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res clientRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewClientRes struct {
|
||||
manager.Client
|
||||
}
|
||||
|
||||
func (res viewClientRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewClientRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewClientRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listClientsRes struct {
|
||||
Clients []manager.Client `json:"clients"`
|
||||
count int
|
||||
}
|
||||
|
||||
func (res listClientsRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listClientsRes) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-Count": fmt.Sprintf("%d", res.count),
|
||||
}
|
||||
}
|
||||
|
||||
func (res listClientsRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res channelRes) code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res channelRes) headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprint("/channels/", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res channelRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type viewChannelRes struct {
|
||||
manager.Channel
|
||||
}
|
||||
|
||||
func (res viewChannelRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewChannelRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewChannelRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listChannelsRes struct {
|
||||
Channels []manager.Channel `json:"channels"`
|
||||
count int
|
||||
}
|
||||
|
||||
func (res listChannelsRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listChannelsRes) headers() map[string]string {
|
||||
return map[string]string{
|
||||
"X-Count": fmt.Sprintf("%d", res.count),
|
||||
}
|
||||
}
|
||||
|
||||
func (res listChannelsRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type connectionRes struct{}
|
||||
|
||||
func (res connectionRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res connectionRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectionRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectionRes struct{}
|
||||
|
||||
func (res disconnectionRes) code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectionRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectionRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc manager.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/users", kithttp.NewServer(
|
||||
registrationEndpoint(svc),
|
||||
decodeCredentials,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/tokens", kithttp.NewServer(
|
||||
loginEndpoint(svc),
|
||||
decodeCredentials,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/clients", kithttp.NewServer(
|
||||
addClientEndpoint(svc),
|
||||
decodeClientCreation,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Put("/clients/:id", kithttp.NewServer(
|
||||
updateClientEndpoint(svc),
|
||||
decodeClientUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/clients/:id", kithttp.NewServer(
|
||||
removeClientEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/clients/:id", kithttp.NewServer(
|
||||
viewClientEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/clients", kithttp.NewServer(
|
||||
listClientsEndpoint(svc),
|
||||
decodeList,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Post("/channels", kithttp.NewServer(
|
||||
createChannelEndpoint(svc),
|
||||
decodeChannelCreation,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Put("/channels/:id", kithttp.NewServer(
|
||||
updateChannelEndpoint(svc),
|
||||
decodeChannelUpdate,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/channels/:id", kithttp.NewServer(
|
||||
removeChannelEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/channels/:id", kithttp.NewServer(
|
||||
viewChannelEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/channels", kithttp.NewServer(
|
||||
listChannelsEndpoint(svc),
|
||||
decodeList,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Put("/channels/:chanId/clients/:clientId", kithttp.NewServer(
|
||||
connectEndpoint(svc),
|
||||
decodeConnection,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/channels/:chanId/clients/:clientId", kithttp.NewServer(
|
||||
disconnectEndpoint(svc),
|
||||
decodeConnection,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/access-grant", kithttp.NewServer(
|
||||
identityEndpoint(svc),
|
||||
decodeIdentity,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/channels/:id/access-grant", kithttp.NewServer(
|
||||
canAccessEndpoint(svc),
|
||||
decodeView,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version())
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeIdentity(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := identityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCredentials(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
var user manager.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userReq{user}, nil
|
||||
}
|
||||
|
||||
func decodeClientCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
var client manager.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := addClientReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
client: client,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeClientUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
var client manager.Client
|
||||
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := updateClientReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
client: client,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeChannelCreation(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
var channel manager.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := createChannelReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
channel: channel,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeChannelUpdate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
var channel manager.Channel
|
||||
if err := json.NewDecoder(r.Body).Decode(&channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := updateChannelReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
channel: channel,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeView(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := viewResourceReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := listResourcesReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
size: 10,
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := connectionReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
chanId: bone.GetValue(r, "chanId"),
|
||||
clientId: bone.GetValue(r, "clientId"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if ar, ok := response.(apiRes); ok {
|
||||
for k, v := range ar.headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.code())
|
||||
|
||||
if ar.empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case manager.ErrMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case manager.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case manager.ErrNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case manager.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
default:
|
||||
if _, ok := err.(*json.SyntaxError); ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package bcrypt provides a hasher implementation utilising bcrypt.
|
||||
package bcrypt
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 channels owned by the specified user.
|
||||
All(string) []Channel
|
||||
|
||||
// Remove removes the channel having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// HasClient determines whether the client with the provided identifier, is
|
||||
// "connected" to the specified channel.
|
||||
HasClient(string, string) bool
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 clients owned by the specified user.
|
||||
All(string) []Client
|
||||
|
||||
// Remove removes the client having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package manager contains the domain concept definitions needed to support
|
||||
// Mainflux manager service functionality.
|
||||
package manager
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package jwt provides a JWT identity provider.
|
||||
package jwt
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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) ([]Client, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.All(sub), nil
|
||||
}
|
||||
|
||||
func (ms *managerService) RemoveClient(key, id string) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.clients.Remove(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) CreateChannel(key string, channel Channel) (string, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = sub
|
||||
return ms.channels.Save(channel)
|
||||
}
|
||||
|
||||
func (ms *managerService) UpdateChannel(key string, channel Channel) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
channel.Owner = sub
|
||||
return ms.channels.Update(channel)
|
||||
}
|
||||
|
||||
func (ms *managerService) ViewChannel(key, id string) (Channel, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return Channel{}, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return Channel{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.One(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) ListChannels(key string) ([]Channel, error) {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return nil, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.All(sub), nil
|
||||
}
|
||||
|
||||
func (ms *managerService) RemoveChannel(key, id string) error {
|
||||
sub, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(sub); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Remove(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) Connect(key, chanId, clientId string) error {
|
||||
owner, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(owner); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Connect(owner, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *managerService) Disconnect(key, chanId, clientId string) error {
|
||||
owner, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(owner); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Disconnect(owner, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *managerService) Identity(key string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (ms *managerService) CanAccess(key, channel string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !ms.channels.HasClient(channel, client) {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,375 +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{ID: "1", Type: "app", Name: "test", Key: "1"}
|
||||
channel manager.Channel = manager.Channel{ID: "1", Name: "test", Clients: []manager.Client{client}}
|
||||
)
|
||||
|
||||
func newService() manager.Service {
|
||||
users := mocks.NewUserRepository()
|
||||
clients := mocks.NewClientRepository()
|
||||
channels := mocks.NewChannelRepository()
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return manager.New(users, clients, channels, hasher, idp)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
svc := newService()
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"register new user": {user, nil},
|
||||
"register existing user": {user, manager.ErrConflict},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Register(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"login with good credentials": {user, nil},
|
||||
"login with wrong e-mail": {manager.User{wrong, user.Password}, manager.ErrUnauthorizedAccess},
|
||||
"login with wrong password": {manager.User{user.Email, wrong}, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.Login(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"add new app": {manager.Client{Type: "app", Name: "a"}, key, nil},
|
||||
"add new device": {manager.Client{Type: "device", Name: "b"}, key, nil},
|
||||
"add client with wrong credentials": {manager.Client{Type: "app", Name: "d"}, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.AddClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing client": {client, key, nil},
|
||||
"update client with wrong credentials": {client, wrong, manager.ErrUnauthorizedAccess},
|
||||
"update non-existing client": {manager.Client{ID: "2", Type: "app", Name: "d"}, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing client": {client.ID, key, nil},
|
||||
"view client with wrong credentials": {client.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"view non-existing client": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"list clients": {key, nil},
|
||||
"list clients with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ListClients(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove client with wrong credentials": {client.ID, "?", manager.ErrUnauthorizedAccess},
|
||||
"remove existing client": {client.ID, key, nil},
|
||||
"remove removed client": {client.ID, key, nil},
|
||||
"remove non-existing client": {"?", key, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"create channel": {manager.Channel{}, key, nil},
|
||||
"create channel with wrong credentials": {manager.Channel{}, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CreateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"update existing channel": {channel, key, nil},
|
||||
"update channel with wrong credentials": {channel, wrong, manager.ErrUnauthorizedAccess},
|
||||
"update non-existing channel": {manager.Channel{ID: "2", Name: "test"}, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"view existing channel": {channel.ID, key, nil},
|
||||
"view channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"view non-existing channel": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"list channels": {key, nil},
|
||||
"list channels with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ListChannels(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"remove channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"remove existing channel": {channel.ID, key, nil},
|
||||
"remove removed channel": {channel.ID, key, nil},
|
||||
"remove non-existing channel": {channel.ID, key, nil},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"connect client": {key, chanId, clientId, nil},
|
||||
"connect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
|
||||
"connect client to non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Connect(tc.key, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
|
||||
svc.Connect(key, chanId, clientId)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"disconnect connected client": {key, chanId, clientId, nil},
|
||||
"disconnect disconnected client": {key, chanId, clientId, manager.ErrNotFound},
|
||||
"disconnect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
|
||||
"disconnect client from non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
|
||||
"disconnect non-existing client": {key, chanId, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Disconnect(tc.key, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid token's identity": {key, nil},
|
||||
"invalid token's identity": {"", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.Identity(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
svc.AddClient(key, client)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
channel string
|
||||
err error
|
||||
}{
|
||||
"allowed access": {client.Key, channel.ID, nil},
|
||||
"not-connected cannot access": {wrong, channel.ID, manager.ErrUnauthorizedAccess},
|
||||
"access non-existing channel": {client.Key, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CanAccess(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepositoryMock)(nil)
|
||||
|
||||
type channelRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
channels map[string]manager.Channel
|
||||
}
|
||||
|
||||
// NewChannelRepository creates in-memory channel repository.
|
||||
func NewChannelRepository() manager.ChannelRepository {
|
||||
return &channelRepositoryMock{
|
||||
channels: make(map[string]manager.Channel),
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
channel.ID = strconv.Itoa(crm.counter)
|
||||
|
||||
crm.channels[key(channel.Owner, channel.ID)] = channel
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Update(channel manager.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(channel.Owner, channel.ID)
|
||||
|
||||
if _, ok := crm.channels[dbKey]; !ok {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
crm.channels[dbKey] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
|
||||
if c, ok := crm.channels[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Channel{}, manager.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) All(owner string) []manager.Channel {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
|
||||
channels := make([]manager.Channel, 0)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
channels = append(channels, v)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.channels, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Since the current implementation has no way to retrieve a real client
|
||||
// instance, the implementation will assume client always exist and create
|
||||
// a dummy one, containing only the provided ID.
|
||||
channel.Clients = append(channel.Clients, manager.Client{ID: clientId})
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !crm.HasClient(chanId, clientId) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
connected := make([]manager.Client, len(channel.Clients)-1)
|
||||
for _, client := range channel.Clients {
|
||||
if client.ID != clientId {
|
||||
connected = append(connected, client)
|
||||
}
|
||||
}
|
||||
|
||||
channel.Clients = connected
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
suffix := fmt.Sprintf("-%s", channel)
|
||||
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasSuffix(k, suffix) {
|
||||
for _, c := range v.Clients {
|
||||
if c.ID == client {
|
||||
return true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepositoryMock)(nil)
|
||||
|
||||
type clientRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
clients map[string]manager.Client
|
||||
}
|
||||
|
||||
// NewClientRepository creates in-memory client repository.
|
||||
func NewClientRepository() manager.ClientRepository {
|
||||
return &clientRepositoryMock{
|
||||
clients: make(map[string]manager.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Id() string {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.counter += 1
|
||||
return strconv.Itoa(crm.counter)
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.clients[key(client.Owner, client.ID)] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Update(client manager.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(client.Owner, client.ID)
|
||||
|
||||
if _, ok := crm.clients[dbKey]; !ok {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
crm.clients[dbKey] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
|
||||
if c, ok := crm.clients[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Client{}, manager.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) All(owner string) []manager.Client {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
|
||||
clients := make([]manager.Client, 0)
|
||||
|
||||
for k, v := range crm.clients {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
clients = append(clients, v)
|
||||
}
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (crm *clientRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.clients, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepository)(nil)
|
||||
|
||||
type channelRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewChannelRepository(db *gorm.DB) manager.ChannelRepository {
|
||||
return &channelRepository{db}
|
||||
}
|
||||
|
||||
func (cr channelRepository) Save(channel manager.Channel) (string, error) {
|
||||
channel.ID = uuid.NewV4().String()
|
||||
|
||||
if err := cr.db.Create(&channel).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Update(channel manager.Channel) error {
|
||||
sql := "UPDATE channels SET name = ? WHERE owner = ? AND id = ?;"
|
||||
res := cr.db.Exec(sql, channel.Name, channel.Owner, channel.ID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
|
||||
channel := manager.Channel{}
|
||||
|
||||
res := cr.db.Preload("Clients").First(&channel, "owner = ? AND id = ?", owner, id)
|
||||
|
||||
if err := res.Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return channel, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) All(owner string) []manager.Channel {
|
||||
var channels []manager.Channel
|
||||
|
||||
cr.db.Find(&channels, "owner = ?", owner)
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func (cr channelRepository) Remove(owner, id string) error {
|
||||
cr.db.Delete(&manager.Channel{}, "owner = ? AND id = ?", owner, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Connect(owner, chanId, clientId string) error {
|
||||
// This approach can be replaced by declaring composite keys on both tables
|
||||
// (clients and channels), and then propagate them into the m2m table. For
|
||||
// some reason GORM does not infer these kind of connections well and
|
||||
// raises a "no unique constraint for referenced table". Until we find a
|
||||
// way to properly represent this relationship, let's stick with the nested
|
||||
// query approach and observe its behaviour.
|
||||
sql := `INSERT INTO channel_clients (channel_id, client_id)
|
||||
SELECT ?, ? WHERE
|
||||
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
|
||||
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
|
||||
|
||||
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) Disconnect(owner, chanId, clientId string) error {
|
||||
// The same remark given in Connect applies here.
|
||||
sql := `DELETE FROM channel_clients WHERE
|
||||
channel_id = ? AND client_id = ? AND
|
||||
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
|
||||
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
|
||||
|
||||
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) HasClient(chanId, clientId string) bool {
|
||||
sql := "SELECT EXISTS (SELECT 1 FROM channel_clients WHERE channel_id = $1 AND client_id = $2);"
|
||||
|
||||
row := cr.db.DB().QueryRow(sql, chanId, clientId)
|
||||
|
||||
var exists bool
|
||||
if err := row.Scan(&exists); err != nil {
|
||||
// TODO: this error should be logged
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChannelSave(t *testing.T) {
|
||||
email := "channel-save@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
c1 := manager.Channel{Owner: email}
|
||||
c2 := manager.Channel{Owner: wrong}
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
hasErr bool
|
||||
}{
|
||||
"new channel, existing user": {c1, false},
|
||||
"new channel, non-existing user": {c2, true},
|
||||
}
|
||||
|
||||
channelRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := channelRepo.Save(tc.channel)
|
||||
hasErr := err != nil
|
||||
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t", desc, tc.hasErr, hasErr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelUpdate(t *testing.T) {
|
||||
email := "channel-update@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
c := manager.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
c.ID = id
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
err error
|
||||
}{
|
||||
"existing channel": {c, nil},
|
||||
"non-existing channel with existing user": {manager.Channel{ID: wrong, Owner: email}, manager.ErrNotFound},
|
||||
"non-existing channel with non-existing user": {manager.Channel{ID: wrong, Owner: wrong}, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Update(tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleChannelRetrieval(t *testing.T) {
|
||||
email := "channel-single-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
c := manager.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, id, nil},
|
||||
"existing user, non-existing channel": {c.Owner, wrong, manager.ErrNotFound},
|
||||
"non-existing owner": {wrong, id, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := chanRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiChannelRetrieval(t *testing.T) {
|
||||
email := "channel-multi-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := manager.Channel{Owner: email}
|
||||
chanRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
len int
|
||||
}{
|
||||
"existing owner": {email, n},
|
||||
"non-existing owner": {wrong, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
n := len(chanRepo.All(tc.owner))
|
||||
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelRemoval(t *testing.T) {
|
||||
email := "channel-removal@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) channel
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := chanRepo.Remove(email, chanId); err != nil {
|
||||
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := chanRepo.One(email, chanId); err != manager.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConnect(t *testing.T) {
|
||||
email := "channel-connect@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"existing user, channel and client": {email, chanId, client.ID, nil},
|
||||
"with non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
|
||||
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Connect(tc.owner, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisconnect(t *testing.T) {
|
||||
email := "channel-disconnect@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"connected client": {email, chanId, client.ID, nil},
|
||||
"non-connected client": {email, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
|
||||
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Disconnect(tc.owner, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelAccessCheck(t *testing.T) {
|
||||
email := "channel-access-check@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
chanId string
|
||||
clientId string
|
||||
hasAccess bool
|
||||
}{
|
||||
"client that has access": {chanId, client.ID, true},
|
||||
"client without access": {chanId, wrong, false},
|
||||
"check access to non-existing channel": {wrong, client.ID, false},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasAccess := chanRepo.HasClient(tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepository)(nil)
|
||||
|
||||
type clientRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewClientRepository instantiates a PostgreSQL implementation of client
|
||||
// repository.
|
||||
func NewClientRepository(db *gorm.DB) manager.ClientRepository {
|
||||
return &clientRepository{db}
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Id() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Save(client manager.Client) error {
|
||||
if err := cr.db.Create(&client).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Update(client manager.Client) error {
|
||||
sql := "UPDATE clients SET name = ?, payload = ? WHERE owner = ? AND id = ?;"
|
||||
res := cr.db.Exec(sql, client.Name, client.Payload, client.Owner, client.ID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
|
||||
client := manager.Client{}
|
||||
|
||||
res := cr.db.First(&client, "owner = ? AND id = ?", owner, id)
|
||||
|
||||
if err := res.Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return client, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return client, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cr *clientRepository) All(owner string) []manager.Client {
|
||||
var clients []manager.Client
|
||||
|
||||
cr.db.Find(&clients, "owner = ?", owner)
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Remove(owner, id string) error {
|
||||
cr.db.Delete(&manager.Client{}, "owner = ? AND id = ?", owner, id)
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientSave(t *testing.T) {
|
||||
email := "client-save@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c1 := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
c2 := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: "unknown@example.com",
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
hasErr bool
|
||||
}{
|
||||
"new client, existing user": {c1, false},
|
||||
"new client, non-existing user": {c2, true},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasErr := clientRepo.Save(tc.client) != nil
|
||||
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasErr, hasErr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientUpdate(t *testing.T) {
|
||||
email := "client-update@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
err error
|
||||
}{
|
||||
"existing client": {c, nil},
|
||||
"non-existing client with existing user": {manager.Client{ID: wrong, Owner: email}, manager.ErrNotFound},
|
||||
"non-existing client with non-existing user": {manager.Client{ID: wrong, Owner: wrong}, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := clientRepo.Update(tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleClientRetrieval(t *testing.T) {
|
||||
email := "client-single-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, c.ID, nil},
|
||||
"existing user, non-existing client": {c.Owner, wrong, manager.ErrNotFound},
|
||||
"non-existing owner": {wrong, c.ID, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := clientRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiClientRetrieval(t *testing.T) {
|
||||
email := "client-multi-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
len int
|
||||
}{
|
||||
"existing owner": {email, n},
|
||||
"non-existing owner": {wrong, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
n := len(clientRepo.All(tc.owner))
|
||||
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemoval(t *testing.T) {
|
||||
email := "client-removal@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) client
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := clientRepo.Remove(email, client.ID); err != nil {
|
||||
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := clientRepo.One(email, client.ID); err != manager.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -1,48 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
const errDuplicate string = "unique_violation"
|
||||
|
||||
type connection struct {
|
||||
ClientID string `gorm:"primary_key"`
|
||||
ChannelID string `gorm:"primary_key"`
|
||||
}
|
||||
|
||||
func (c connection) TableName() string {
|
||||
return "channel_clients"
|
||||
}
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance. A non-nil error
|
||||
// is returned to indicate failure.
|
||||
func Connect(host, port, name, user, pass string) (*gorm.DB, error) {
|
||||
t := "host=%s port=%s user=%s dbname=%s password=%s sslmode=disable"
|
||||
url := fmt.Sprintf(t, host, port, user, name, pass)
|
||||
|
||||
db, err := gorm.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db = db.AutoMigrate(&manager.User{}, &manager.Client{}, &manager.Channel{}, &connection{})
|
||||
|
||||
db = db.Model(&manager.Client{}).
|
||||
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
|
||||
AddUniqueIndex("idx_pk_client", "id", "owner")
|
||||
|
||||
db = db.Model(&manager.Channel{}).
|
||||
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
|
||||
AddUniqueIndex("idx_pk_channel", "id", "owner")
|
||||
|
||||
db = db.Model(&connection{}).
|
||||
AddForeignKey("client_id", "clients(id)", "CASCADE", "CASCADE").
|
||||
AddForeignKey("channel_id", "channels(id)", "CASCADE", "CASCADE")
|
||||
|
||||
return db.LogMode(false), nil
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Package postgres_test contains tests for PostgreSQL repository
|
||||
// implementations.
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect("localhost", port, "test", "test", "test"); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.UserRepository = (*userRepository)(nil)
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository instantiates a PostgreSQL implementation of user
|
||||
// repository.
|
||||
func NewUserRepository(db *gorm.DB) manager.UserRepository {
|
||||
return &userRepository{db}
|
||||
}
|
||||
|
||||
func (ur *userRepository) Save(user manager.User) error {
|
||||
if err := ur.db.Create(&user).Error; err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && errDuplicate == pqErr.Code.Name() {
|
||||
return manager.ErrConflict
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur *userRepository) One(email string) (manager.User, error) {
|
||||
user := manager.User{}
|
||||
|
||||
q := ur.db.First(&user, "email = ?", email)
|
||||
|
||||
if err := q.Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return user, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserSave(t *testing.T) {
|
||||
email := "user-save@example.com"
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"new user": {manager.User{email, "pass"}, nil},
|
||||
"duplicate user": {manager.User{email, "pass"}, manager.ErrConflict},
|
||||
}
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := repo.Save(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleUserRetrieval(t *testing.T) {
|
||||
email := "user-retrieval@example.com"
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
repo.Save(manager.User{email, "pass"})
|
||||
|
||||
cases := map[string]struct {
|
||||
email string
|
||||
err error
|
||||
}{
|
||||
"existing user": {email, nil},
|
||||
"non-existing user": {"unknown@example.com", manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := repo.One(tc.email)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package manager
|
||||
|
||||
// Hasher specifies an API for generating hashes of an arbitrary textual
|
||||
// content.
|
||||
type Hasher interface {
|
||||
// Hash generates the hashed string from plain-text.
|
||||
Hash(string) (string, error)
|
||||
|
||||
// Compare compares plain-text version to the hashed one. An error should
|
||||
// indicate failed comparison.
|
||||
Compare(string, string) error
|
||||
}
|
||||
|
||||
// IdentityProvider specifies an API for identity management via security
|
||||
// tokens.
|
||||
type IdentityProvider interface {
|
||||
// TemporaryKey generates the temporary access token.
|
||||
TemporaryKey(string) (string, error)
|
||||
|
||||
// PermanentKey generates the non-expiring access token.
|
||||
PermanentKey(string) (string, error)
|
||||
|
||||
// Identity extracts the entity identifier given its secret key.
|
||||
Identity(string) (string, error)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package manager
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrConflict indicates usage of the existing email during account
|
||||
// registration.
|
||||
ErrConflict error = errors.New("email already taken")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||||
// invalid username or password).
|
||||
ErrMalformedEntity error = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess error = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound error = errors.New("non-existent entity")
|
||||
)
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Register creates new user account. In case of the failed registration, a
|
||||
// non-nil error value is returned.
|
||||
Register(User) error
|
||||
|
||||
// Login authenticates the user given its credentials. Successful
|
||||
// authentication generates new access token. Failed invocations are
|
||||
// identified by the non-nil error values in the response.
|
||||
Login(User) (string, error)
|
||||
|
||||
// AddClient adds new client to the user identified by the provided key.
|
||||
AddClient(string, Client) (string, error)
|
||||
|
||||
// UpdateClient updates the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
UpdateClient(string, Client) error
|
||||
|
||||
// ViewClient retrieves data about the client identified with the provided
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewClient(string, string) (Client, error)
|
||||
|
||||
// ListClients retrieves data about all clients that belongs to the user
|
||||
// identified by the provided key.
|
||||
ListClients(string) ([]Client, error)
|
||||
|
||||
// RemoveClient removes the client identified with the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveClient(string, string) error
|
||||
|
||||
// CreateChannel adds new channel to the user identified by the provided key.
|
||||
CreateChannel(string, Channel) (string, error)
|
||||
|
||||
// UpdateChannel updates the channel identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
UpdateChannel(string, Channel) error
|
||||
|
||||
// ViewChannel retrieves data about the channel identified by the provided
|
||||
// ID, that belongs to the user identified by the provided key.
|
||||
ViewChannel(string, string) (Channel, error)
|
||||
|
||||
// ListChannels retrieves data about all clients that belongs to the user
|
||||
// identified by the provided key.
|
||||
ListChannels(string) ([]Channel, error)
|
||||
|
||||
// RemoveChannel removes the client identified by the provided ID, that
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveChannel(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// Identity retrieves Client ID for provided client token.
|
||||
Identity(string) (string, error)
|
||||
|
||||
// CanAccess determines whether the channel can be accessed using the
|
||||
// provided key.
|
||||
CanAccess(string, string) (string, error)
|
||||
}
|
||||
@@ -1,548 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux manager service
|
||||
description: HTTP API for managing platform users, devices, applications and channels.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/users:
|
||||
post:
|
||||
summary: Registers user account
|
||||
description: |
|
||||
Registers new user account given email and password. New account will
|
||||
be uniquely identified by its email address.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- name: user
|
||||
description: JSON-formatted document describing the new user.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Registered new user.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using an existing email address.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/tokens:
|
||||
post:
|
||||
summary: User authentication
|
||||
description: |
|
||||
Generates an access token when provided with proper credentials.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
- name: credentials
|
||||
description: JSON-formatted document containing user credentials.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: User authenticated.
|
||||
schema:
|
||||
$ref: "#/definitions/Token"
|
||||
400:
|
||||
description: |
|
||||
Failed due to malformed JSON or using an invalid credentials.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/clients:
|
||||
post:
|
||||
summary: Adds new client
|
||||
description: |
|
||||
Adds new client to the list of clients owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: client
|
||||
description: JSON-formatted document describing the new client.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ClientReq"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Client registered.
|
||||
headers:
|
||||
Location:
|
||||
type: string
|
||||
description: Created client's relative URL (i.e. /clients/{clientId}).
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed clients
|
||||
description: |
|
||||
Retrieves a list of managed clients. Due to performance concerns, data
|
||||
is retrieved in subsets. The API clients must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Size"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
headers:
|
||||
X-Count:
|
||||
type: integer
|
||||
description: |
|
||||
Total number of managed clients. This value can be used to
|
||||
implement the paging strategy on API clients.
|
||||
schema:
|
||||
$ref: "#/definitions/ClientList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/clients/{clientId}:
|
||||
get:
|
||||
summary: Retrieves client info
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ClientRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates client info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the client's type and ID
|
||||
cannot be changed.
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
- name: client
|
||||
description: JSON-formatted document describing the updated client.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ClientReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Client updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a client
|
||||
description: |
|
||||
Removes a client. The service will ensure that the removed client is
|
||||
disconnected from all of the existing channels.
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
204:
|
||||
description: Client removed.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/channels:
|
||||
post:
|
||||
summary: Creates new channel
|
||||
description: |
|
||||
Creates new channel. User identified by the provided access token will
|
||||
be the channel's owner.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: channel
|
||||
description: JSON-formatted document describing the new channel.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ChannelReq"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Channel created.
|
||||
headers:
|
||||
Location:
|
||||
type: string
|
||||
description: Created channel's relative URL (i.e. /channels/{chanId}).
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed channels
|
||||
description: |
|
||||
Retrieves a list of managed channels. Due to performance concerns, data
|
||||
is retrieved in subsets. The API clients must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Size"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
headers:
|
||||
X-Count:
|
||||
type: integer
|
||||
description: |
|
||||
Total number of managed channels. This value can be used to
|
||||
implement the paging strategy on API clients.
|
||||
schema:
|
||||
$ref: "#/definitions/ChannelList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/channels/{chanId}:
|
||||
get:
|
||||
summary: Retrieves channel info
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ChannelRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates channel info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the channel's ID will not be
|
||||
affected.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
- name: channel
|
||||
description: JSON-formatted document describing the updated channel.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ChannelReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Channel updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a channel
|
||||
description: |
|
||||
Removes a channel. The service will ensure that the subscribed apps and
|
||||
devices are unsubscribed from the removed channel.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
204:
|
||||
description: Channel removed.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/channels/{chanId}/clients/{clientId}:
|
||||
put:
|
||||
summary: Connects the client to the channel
|
||||
description: |
|
||||
Creates connection between a client and a channel. Once connected to
|
||||
the channel, clients are allowed to exchange messages through it.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
200:
|
||||
description: Client connected.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel or client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Disconnects the client from the channel
|
||||
description: |
|
||||
Removes connection between a client and a channel. Once connection is
|
||||
removed, client can no longer exchange messages through the channel.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
204:
|
||||
description: Client disconnected.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel or client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/access-grant:
|
||||
get:
|
||||
summary: Checks the token validity
|
||||
description: |
|
||||
Internal endpoint used to verify requests forwarded by stateful adapters
|
||||
like MQTT. If the request is made using valid token, an identifier bound
|
||||
to that token will be returned.
|
||||
tags:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
responses:
|
||||
200:
|
||||
description: ID retrieved
|
||||
headers:
|
||||
X-client-id:
|
||||
type: string
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
/channels/{chanId}/access-grant:
|
||||
get:
|
||||
summary: Checks channel accessibility
|
||||
description: |
|
||||
Internal endpoint used to determine whether or not a channel can be
|
||||
accessed using credentials provided as part of the request.
|
||||
tags:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
description: Client can access the channel.
|
||||
headers:
|
||||
X-client-id:
|
||||
type: string
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Client cannot access the channel.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ClientId:
|
||||
name: clientId
|
||||
description: Unique client identifier.
|
||||
in: path
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Size:
|
||||
name: size
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
type: integer
|
||||
default: 10
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
required: false
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occured.
|
||||
|
||||
definitions:
|
||||
ChannelList:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique channel identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form channel name.
|
||||
required:
|
||||
- id
|
||||
required:
|
||||
- channels
|
||||
ChannelRes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique channel identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form channel name.
|
||||
connected:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: '#/definitions/ClientRes'
|
||||
required:
|
||||
- id
|
||||
ChannelReq:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Free-form channel name.
|
||||
ClientList:
|
||||
type: object
|
||||
properties:
|
||||
clients:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/definitions/ClientRes"
|
||||
required:
|
||||
- clients
|
||||
ClientRes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique client identifier generated by the service.
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- app
|
||||
- device
|
||||
description: Type of the client.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form client name.
|
||||
key:
|
||||
type: string
|
||||
description: Auto-generated access key.
|
||||
payload:
|
||||
type: string
|
||||
description: Arbitrary, string-encoded client's data.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
- key
|
||||
ClientReq:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- app
|
||||
- device
|
||||
description: Type of the client.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form client name.
|
||||
payload:
|
||||
type: string
|
||||
description: Arbitrary, string-encoded client's data.
|
||||
required:
|
||||
- type
|
||||
Token:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: Generated access token.
|
||||
required:
|
||||
- token
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: "test@example.com"
|
||||
description: User's email address will be used as its unique identifier
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
description: Free-form account password used for acquiring auth token(s).
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
@@ -1,33 +0,0 @@
|
||||
package manager
|
||||
|
||||
import "github.com/asaskevich/govalidator"
|
||||
|
||||
// User represents a Mainflux user account. Each user is identified given its
|
||||
// email and password.
|
||||
type User struct {
|
||||
Email string `gorm:"type:varchar(254);primary_key"`
|
||||
Password string `gorm:"type:char(60)"`
|
||||
}
|
||||
|
||||
// Validate returns an error if user representation is invalid.
|
||||
func (u *User) Validate() error {
|
||||
if u.Email == "" || u.Password == "" {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
|
||||
if !govalidator.IsEmail(u.Email) {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserRepository specifies an account persistence API.
|
||||
type UserRepository interface {
|
||||
// Save persists the user account. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Save(User) error
|
||||
|
||||
// One retrieves user by its unique identifier (i.e. email).
|
||||
One(string) (User, error)
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
package mainflux
|
||||
|
||||
// Message represents a resolved (normalized) raw message.
|
||||
type Message struct {
|
||||
Channel string
|
||||
Publisher string
|
||||
Protocol string
|
||||
Name string `json:"n,omitempty"`
|
||||
Unit string `json:"u,omitempty"`
|
||||
Value float64 `json:"v,omitempty"`
|
||||
StringValue string `json:"vs,omitempty"`
|
||||
BoolValue bool `json:"vb,omitempty"`
|
||||
DataValue string `json:"vd,omitempty"`
|
||||
ValueSum float64 `json:"s,omitempty"`
|
||||
Time float64 `json:"t,omitempty"`
|
||||
UpdateTime float64 `json:"ut,omitempty"`
|
||||
Link string `json:"l,omitempty"`
|
||||
}
|
||||
|
||||
// RawMessage represents a message emitted by the mainflux adapters layer.
|
||||
type RawMessage struct {
|
||||
Channel string `json:"channel"`
|
||||
Publisher string `json:"publisher"`
|
||||
Protocol string `json:"protocol"`
|
||||
ContentType string `json:"content_type"`
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
// MessagePublisher specifies a message publishing API.
|
||||
type MessagePublisher interface {
|
||||
// Publishes message to the stream. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Publish(RawMessage) error
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# Message normalizer
|
||||
|
||||
Normalizer service consumes events published by adapters, normalizes SenML-formatted
|
||||
ones, and publishes them to the post-processing stream.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|--------------------|------------------------------|-----------------------|
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_NORMALIZER_PORT | Normalizer service HTTP port | 8180 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
manager:
|
||||
image: mainflux/normalizer:[version]
|
||||
container_name: [instance name]
|
||||
environment:
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_NORMALIZER_PORT: [Service HTTP port]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/normalizer
|
||||
|
||||
# compile the app; make sure to set the proper GOOS value
|
||||
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_NATS_URL=[NATS instance URL] MF_NORMALIZER_PORT=[Service HTTP port] app
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
package normalizer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler() http.Handler {
|
||||
r := bone.New()
|
||||
r.GetFunc("/version", mainflux.Version())
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package normalizer contains the domain concept definitions needed to
|
||||
// support Mainflux normalizer service functionality.
|
||||
package normalizer
|
||||
@@ -1,112 +0,0 @@
|
||||
package normalizer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/cisco/senml"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
queue string = "normalizers"
|
||||
subject string = "src.*"
|
||||
output string = "normalized"
|
||||
)
|
||||
|
||||
type eventFlow struct {
|
||||
nc *nats.Conn
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// Subscribe instantiates and starts a new NATS message flow.
|
||||
func Subscribe(nc *nats.Conn, logger log.Logger) {
|
||||
flow := eventFlow{nc, logger}
|
||||
flow.start()
|
||||
}
|
||||
|
||||
func (ef eventFlow) start() {
|
||||
ef.nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
|
||||
msg := mainflux.RawMessage{}
|
||||
|
||||
if err := json.Unmarshal(m.Data, &msg); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Unmarshalling failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := ef.publish(msg); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ef eventFlow) publish(msg mainflux.RawMessage) error {
|
||||
normalized, err := ef.normalize(msg)
|
||||
if err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Normalization failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range normalized {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Marshalling failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ef.nc.Publish(subject, data); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ef eventFlow) normalize(msg mainflux.RawMessage) ([]mainflux.Message, error) {
|
||||
var (
|
||||
raw, normalized senml.SenML
|
||||
err error
|
||||
)
|
||||
|
||||
if raw, err = senml.Decode(msg.Payload, senml.JSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
normalized = senml.Normalize(raw)
|
||||
|
||||
msgs := make([]mainflux.Message, len(normalized.Records))
|
||||
for k, v := range normalized.Records {
|
||||
m := mainflux.Message{
|
||||
Channel: msg.Channel,
|
||||
Publisher: msg.Publisher,
|
||||
Protocol: msg.Protocol,
|
||||
Name: v.Name,
|
||||
Unit: v.Unit,
|
||||
StringValue: v.StringValue,
|
||||
DataValue: v.DataValue,
|
||||
Time: v.Time,
|
||||
UpdateTime: v.UpdateTime,
|
||||
Link: v.Link,
|
||||
}
|
||||
|
||||
if v.Value != nil {
|
||||
m.Value = *v.Value
|
||||
}
|
||||
|
||||
if v.BoolValue != nil {
|
||||
m.BoolValue = *v.BoolValue
|
||||
}
|
||||
|
||||
if v.Sum != nil {
|
||||
m.ValueSum = *v.Sum
|
||||
}
|
||||
|
||||
msgs[k] = m
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Notifiers service
|
||||
description: |
|
||||
HTTP API for Notifiers service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9014
|
||||
- url: https://localhost:9014
|
||||
- url: http://localhost:9015
|
||||
- url: https://localhost:9015
|
||||
|
||||
tags:
|
||||
- name: notifiers
|
||||
description: Everything about your Notifiers
|
||||
externalDocs:
|
||||
description: Find out more about notifiers
|
||||
url: https://magistrala.absmach.eu/docs/
|
||||
|
||||
paths:
|
||||
/subscriptions:
|
||||
post:
|
||||
operationId: createSubscription
|
||||
summary: Create subscription
|
||||
description: Creates a new subscription give a topic and contact.
|
||||
tags:
|
||||
- notifiers
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/Create"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/Create"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"409":
|
||||
description: Failed due to using an existing topic and contact.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
operationId: listSubscriptions
|
||||
summary: List subscriptions
|
||||
description: List subscriptions given list parameters.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Topic"
|
||||
- $ref: "#/components/parameters/Contact"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Page"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/subscriptions/{id}:
|
||||
get:
|
||||
operationId: viewSubscription
|
||||
summary: Get subscription with the provided id
|
||||
description: Retrieves a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/View"
|
||||
"400":
|
||||
description: Failed due to malformed ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeSubscription
|
||||
summary: Delete subscription with the provided id
|
||||
description: Removes a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"204":
|
||||
description: Subscription removed
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Subscription:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
example: 01EWDVKBQSG80B6PQRS9PAAY35
|
||||
description: ULID id of the subscription.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: An id of the owner who created subscription.
|
||||
topic:
|
||||
type: string
|
||||
example: topic/subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
CreateSubscription:
|
||||
type: object
|
||||
properties:
|
||||
topic:
|
||||
type: string
|
||||
example: topic/subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
Page:
|
||||
type: object
|
||||
properties:
|
||||
subscriptions:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
|
||||
parameters:
|
||||
Id:
|
||||
name: id
|
||||
description: Unique identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Topic:
|
||||
name: topic
|
||||
description: Topic name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Contact:
|
||||
name: contact
|
||||
description: Subscription contact.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
Create:
|
||||
description: JSON-formatted document describing the new subscription to be created
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateSubscription"
|
||||
|
||||
responses:
|
||||
Create:
|
||||
description: Created a new subscription.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created subscription relative URL
|
||||
example: /subscriptions/{id}
|
||||
View:
|
||||
description: View subscription.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
links:
|
||||
delete:
|
||||
operationId: removeSubscription
|
||||
parameters:
|
||||
id: $response.body#/id
|
||||
Page:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Page"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala reader service
|
||||
description: |
|
||||
HTTP API for reading messages.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9003
|
||||
- url: https://localhost:9003
|
||||
- url: http://localhost:9005
|
||||
- url: https://localhost:9005
|
||||
- url: http://localhost:9009
|
||||
- url: https://localhost:9009
|
||||
- url: http://localhost:9011
|
||||
- url: https://localhost:9011
|
||||
|
||||
tags:
|
||||
- name: readers
|
||||
description: Everything about your Readers
|
||||
externalDocs:
|
||||
description: Find out more about readers
|
||||
url: https://magistrala.absmach.eu/docs/
|
||||
|
||||
paths:
|
||||
/{domainID}/channels/{chanId}/messages:
|
||||
get:
|
||||
operationId: getMessages
|
||||
summary: Retrieves messages sent to single channel
|
||||
description: |
|
||||
Retrieves a list of messages sent to specific channel. Due to
|
||||
performance concerns, data is retrieved in subsets. The API readers must
|
||||
ensure that the entire dataset is consumed either by making subsequent
|
||||
requests, or by increasing the subset size of the initial request.
|
||||
tags:
|
||||
- readers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Publisher"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Value"
|
||||
- $ref: "#/components/parameters/BoolValue"
|
||||
- $ref: "#/components/parameters/StringValue"
|
||||
- $ref: "#/components/parameters/DataValue"
|
||||
- $ref: "#/components/parameters/From"
|
||||
- $ref: "#/components/parameters/To"
|
||||
- $ref: "#/components/parameters/Aggregation"
|
||||
- $ref: "#/components/parameters/Interval"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MessagesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
operationId: health
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
MessagesPage:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: number
|
||||
description: Total number of items that are present on the system.
|
||||
offset:
|
||||
type: number
|
||||
description: Number of items that were skipped during retrieval.
|
||||
limit:
|
||||
type: number
|
||||
description: Size of the subset that was retrieved.
|
||||
messages:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
channel:
|
||||
type: integer
|
||||
description: Unique channel id.
|
||||
publisher:
|
||||
type: integer
|
||||
description: Unique publisher id.
|
||||
protocol:
|
||||
type: string
|
||||
description: Protocol name.
|
||||
name:
|
||||
type: string
|
||||
description: Measured parameter name.
|
||||
unit:
|
||||
type: string
|
||||
description: Value unit.
|
||||
value:
|
||||
type: number
|
||||
description: Measured value in number.
|
||||
stringValue:
|
||||
type: string
|
||||
description: Measured value in string format.
|
||||
boolValue:
|
||||
type: boolean
|
||||
description: Measured value in boolean format.
|
||||
dataValue:
|
||||
type: string
|
||||
description: Measured value in binary format.
|
||||
valueSum:
|
||||
type: number
|
||||
description: Sum value.
|
||||
time:
|
||||
type: number
|
||||
description: Time of measurement.
|
||||
updateTime:
|
||||
type: number
|
||||
description: Time of updating measurement.
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Publisher:
|
||||
name: Publisher
|
||||
description: Unique thing identifier.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: SenML message name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Value:
|
||||
name: v
|
||||
description: SenML message value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
BoolValue:
|
||||
name: vb
|
||||
description: SenML message bool value.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
StringValue:
|
||||
name: vs
|
||||
description: SenML message string value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
DataValue:
|
||||
name: vd
|
||||
description: SenML message data value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Comparator:
|
||||
name: comparator
|
||||
description: Value comparison operator.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: eq
|
||||
enum:
|
||||
- eq
|
||||
- lt
|
||||
- le
|
||||
- gt
|
||||
- ge
|
||||
required: false
|
||||
From:
|
||||
name: from
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218556069
|
||||
required: false
|
||||
To:
|
||||
name: to
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218757503
|
||||
required: false
|
||||
Aggregation:
|
||||
name: aggregation
|
||||
description: Aggregation function.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- MAX
|
||||
- AVG
|
||||
- MIN
|
||||
- SUM
|
||||
- COUNT
|
||||
- max
|
||||
- min
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
example: MAX
|
||||
required: false
|
||||
Interval:
|
||||
name: interval
|
||||
description: Aggregation interval.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
example: 10s
|
||||
required: false
|
||||
|
||||
responses:
|
||||
MessagesPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
thingAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <thing_key>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- thingAuth: []
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Reports Service API
|
||||
description: |
|
||||
HTTP API for managing reports service.
|
||||
version: 0.18.5
|
||||
servers:
|
||||
- url: http://localhost:9017
|
||||
tags:
|
||||
- name: reports
|
||||
description: Operations related to report configurations and generation
|
||||
paths:
|
||||
/{domainID}/reports:
|
||||
post:
|
||||
operationId: generateReport
|
||||
summary: Generate a report
|
||||
description: Generates a report based on the provided configuration or an existing config. The action determines the response format.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenerateReportRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Report generated successfully (content varies by action)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenerateReportResponse'
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'400':
|
||||
description: Invalid request parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs:
|
||||
post:
|
||||
operationId: addReportConfig
|
||||
summary: Create a report configuration
|
||||
description: Creates a new report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddReportConfigRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Report configuration created
|
||||
headers:
|
||||
Location:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'400':
|
||||
description: Invalid request body
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
get:
|
||||
operationId: listReportConfigs
|
||||
summary: List report configurations
|
||||
description: Retrieves a paginated list of report configurations.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: List of report configurations
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ListReportsConfigResponse'
|
||||
'400':
|
||||
description: Invalid query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}:
|
||||
get:
|
||||
operationId: viewReportConfig
|
||||
summary: View a report configuration
|
||||
description: Retrieves details of a specific report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
patch:
|
||||
operationId: updateReportConfig
|
||||
summary: Update a report configuration
|
||||
description: Updates specified fields of a report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateReportConfigRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'400':
|
||||
description: Invalid request body
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
delete:
|
||||
operationId: deleteReportConfig
|
||||
summary: Delete a report configuration
|
||||
description: Permanently deletes a report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Report configuration deleted
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}/schedule:
|
||||
patch:
|
||||
operationId: updateReportSchedule
|
||||
summary: Update report schedule
|
||||
description: Updates the schedule of a report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
responses:
|
||||
'200':
|
||||
description: Schedule updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'400':
|
||||
description: Invalid schedule
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}/enable:
|
||||
post:
|
||||
operationId: enableReportConfig
|
||||
summary: Enable a report configuration
|
||||
description: Enables a report configuration to generate scheduled reports.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration enabled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}/disable:
|
||||
post:
|
||||
operationId: disableReportConfig
|
||||
summary: Disable a report configuration
|
||||
description: Disables a report configuration, stopping scheduled reports.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration disabled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Service health check
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/HealthRes'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ReportConfig:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
domain_id:
|
||||
type: string
|
||||
readOnly: true
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
status:
|
||||
$ref: '#/components/schemas/Status'
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
created_by:
|
||||
type: string
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- name
|
||||
- metrics
|
||||
- config
|
||||
|
||||
Schedule:
|
||||
type: object
|
||||
properties:
|
||||
recurring:
|
||||
type: string
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
next_run:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
|
||||
MetricConfig:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
maxLength: 100
|
||||
format:
|
||||
type: string
|
||||
enum: [pdf, csv, html]
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/AggConfig'
|
||||
|
||||
AggConfig:
|
||||
type: object
|
||||
properties:
|
||||
window:
|
||||
type: string
|
||||
function:
|
||||
type: string
|
||||
enum: [sum, average, max, min]
|
||||
|
||||
EmailSetting:
|
||||
type: object
|
||||
properties:
|
||||
recipients:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: email
|
||||
subject:
|
||||
type: string
|
||||
body_template:
|
||||
type: string
|
||||
required:
|
||||
- recipients
|
||||
- subject
|
||||
|
||||
ReqMetric:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [gauge, counter, histogram]
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
|
||||
Status:
|
||||
type: string
|
||||
enum: [enabled, disabled]
|
||||
|
||||
GenerateReportRequest:
|
||||
type: object
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
enum: [view, download, email]
|
||||
config_id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
required:
|
||||
- action
|
||||
|
||||
GenerateReportResponse:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
from:
|
||||
type: string
|
||||
format: date-time
|
||||
to:
|
||||
type: string
|
||||
format: date-time
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/AggConfig'
|
||||
reports:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Report'
|
||||
|
||||
Report:
|
||||
type: object
|
||||
properties:
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
value:
|
||||
type: number
|
||||
metric_name:
|
||||
type: string
|
||||
|
||||
AddReportConfigRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
status:
|
||||
$ref: '#/components/schemas/Status'
|
||||
required:
|
||||
- name
|
||||
- metrics
|
||||
- config
|
||||
|
||||
UpdateReportConfigRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
status:
|
||||
$ref: '#/components/schemas/Status'
|
||||
|
||||
ListReportsConfigResponse:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
report_configs:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
ReportID:
|
||||
name: reportID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server error
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: './schemas/health_info.yaml'
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
+586
@@ -0,0 +1,586 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Rules Engine API
|
||||
description: |
|
||||
HTTP API for managing rules engine service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9008
|
||||
- url: http://localhost:9008
|
||||
|
||||
tags:
|
||||
- name: rules engine
|
||||
description: Everything about your Rules Engine
|
||||
externalDocs:
|
||||
description: Find out more about rules engine
|
||||
url: https://magistrala.absmach.eu/docs/
|
||||
|
||||
paths:
|
||||
/{domainID}/rules:
|
||||
post:
|
||||
operationId: createRule
|
||||
summary: Create Rule
|
||||
description: |
|
||||
Creates a new rule for message processing
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/RuleCreateReq'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/components/responses/RuleCreateRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'415':
|
||||
description: Missing or invalid content type
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
"503":
|
||||
description: Failed to receive response from the clients service.
|
||||
get:
|
||||
operationId: getRules
|
||||
summary: List Rules
|
||||
description: |
|
||||
Retrieves a list of rules with optional filtering
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/InputChannel'
|
||||
- $ref: '#/components/parameters/OutputChannel'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleListRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/rules/{ruleID}:
|
||||
get:
|
||||
operationId: getRule
|
||||
summary: View Rule
|
||||
description: Retrieves a rule by ID
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleRes'
|
||||
"400":
|
||||
description: Missing or invalid rule
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"422":
|
||||
description: Database can't process request
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateRule
|
||||
summary: Update Rule
|
||||
description: Updates an existing rule
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/RuleUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeRule
|
||||
summary: Delete Rule
|
||||
description: Deletes a rule
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Rule deleted successfully
|
||||
"400":
|
||||
description: Failed due to malformed rule ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
"422":
|
||||
description: Database can't process request
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/rules/{ruleID}/enable:
|
||||
put:
|
||||
operationId: enableRule
|
||||
summary: Enable Rule
|
||||
description: Enables a rule for processing
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Rule enabled successfully
|
||||
"400":
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/rules/{ruleID}/disable:
|
||||
put:
|
||||
operationId: disableRule
|
||||
summary: Disable Rule
|
||||
description: Disables a rule from processing
|
||||
tags:
|
||||
- Rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Rule disabled successfully
|
||||
"400":
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"422":
|
||||
description: Database can't process request
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
RulesListRes:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve
|
||||
maximum: 100
|
||||
default: 10
|
||||
rules:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: '#/components/schemas/Rule'
|
||||
required:
|
||||
- rules
|
||||
|
||||
Rule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique rule identifier
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
domain:
|
||||
type: string
|
||||
description: Domain ID this rule belongs to
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions (1 = every interval, 2 = every second interval, etc.)
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
readOnly: true
|
||||
created_by:
|
||||
type: string
|
||||
description: User who created the rule
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
description: User who last updated the rule
|
||||
required:
|
||||
- name
|
||||
- domain
|
||||
- input_channel
|
||||
- input_topic
|
||||
- logic
|
||||
- status
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Domain ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
RuleID:
|
||||
name: ruleID
|
||||
description: Rule ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
InputChannel:
|
||||
name: input_channel
|
||||
description: Filter by input channel
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
OutputChannel:
|
||||
name: output_channel
|
||||
description: Filter by output channel
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Status:
|
||||
name: status
|
||||
description: Filter by rule status
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [enabled, disabled]
|
||||
default: enabled
|
||||
|
||||
requestBodies:
|
||||
RuleCreateReq:
|
||||
description: JSON-formatted document describing the new rule
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
domain:
|
||||
type: string
|
||||
description: Domain ID this rule belongs to
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
required:
|
||||
- name
|
||||
- domain
|
||||
- input_channel
|
||||
- input_topic
|
||||
- logic
|
||||
- schedule
|
||||
RuleUpdateReq:
|
||||
description: JSON-formatted document describing the rule update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
|
||||
responses:
|
||||
RuleCreateRes:
|
||||
description: Rule registered
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created rule's relative URL (i.e. /rules/{ruleID})
|
||||
RuleListRes:
|
||||
description: Data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RulesListRes'
|
||||
RuleRes:
|
||||
description: Data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Rule'
|
||||
links:
|
||||
update:
|
||||
operationId: updateRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
enable:
|
||||
operationId: enableRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
disable:
|
||||
operationId: disableRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
delete:
|
||||
operationId: removeRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred
|
||||
HealthRes:
|
||||
description: Service Health Check
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Service status.
|
||||
enum:
|
||||
- pass
|
||||
version:
|
||||
type: string
|
||||
description: Service version.
|
||||
example: v0.18.0
|
||||
commit:
|
||||
type: string
|
||||
description: Service commit hash.
|
||||
example: 73362210dd2e04e389eaddb802cab3fe03976593
|
||||
description:
|
||||
type: string
|
||||
description: Service description.
|
||||
example: <service_name> service
|
||||
build_time:
|
||||
type: string
|
||||
description: Service build time.
|
||||
example: 2024-02-01_12:18:15
|
||||
instance_id:
|
||||
type: string
|
||||
description: Service instance ID.
|
||||
example: 8edbf8af-7db7-4218-bb4f-a8a929ff5266
|
||||
@@ -0,0 +1,395 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
components:
|
||||
schemas:
|
||||
NewRole:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role unique identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
example: "roleName"
|
||||
entity_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Entity unique identifier.
|
||||
created_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role creator unique identifier.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
updated_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: ""
|
||||
description: Role updater unique identifier.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: ""
|
||||
description: Time when the channel was created.
|
||||
|
||||
Role:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role unique identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
example: "roleName"
|
||||
entity_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Entity unique identifier.
|
||||
created_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role creator unique identifier.
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
updated_by:
|
||||
type: string
|
||||
format: uuid
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
description: Role updater unique identifier.
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the channel was created.
|
||||
|
||||
CreateRoleObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
example: "roleName"
|
||||
optional_actions:
|
||||
type: array
|
||||
description: List of optional actions.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"read",
|
||||
"update",
|
||||
]
|
||||
optional_members:
|
||||
type: array
|
||||
minItems: 0
|
||||
description: List of optional members.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb",
|
||||
"c01ed106-e52d-4aa4-bed3-39f360177cfa",
|
||||
]
|
||||
|
||||
UpdateRoleObj:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Role's name.
|
||||
|
||||
RolesPage:
|
||||
type: object
|
||||
properties:
|
||||
roles:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Role'
|
||||
description: List of roles.
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of roles.
|
||||
example: 1
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
|
||||
EntityMembersPage:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/EntityMembersObj"
|
||||
total:
|
||||
type: integer
|
||||
example: 1
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- groups
|
||||
- total
|
||||
- offset
|
||||
|
||||
RoleActionsObj:
|
||||
type: object
|
||||
properties:
|
||||
actions:
|
||||
type: array
|
||||
description: List of actions to be added to a role.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"read",
|
||||
"update",
|
||||
]
|
||||
|
||||
RoleMembersObj:
|
||||
type: object
|
||||
properties:
|
||||
members:
|
||||
type: array
|
||||
description: List of members to be added to a role.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb",
|
||||
"c01ed106-e52d-4aa4-bed3-39f360177cfa",
|
||||
]
|
||||
|
||||
EntityMembersObj:
|
||||
type: object
|
||||
properties:
|
||||
members:
|
||||
type: array
|
||||
description: List of members with assigned roles and actions.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of the member.
|
||||
roles:
|
||||
type: array
|
||||
description: List of roles assigned to the member.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of the role.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the role.
|
||||
actions:
|
||||
type: array
|
||||
description: List of actions the member can perform.
|
||||
items:
|
||||
type: string
|
||||
access_type:
|
||||
type: string
|
||||
description: Type of access granted.
|
||||
enum: [read, write, admin] # Adjust based on your actual access types.
|
||||
example:
|
||||
members:
|
||||
- id: "5dc1ce4b-7cc9-4f12-98a6-9d74cc4980bb"
|
||||
roles:
|
||||
- id: "a1b2c3d4-e5f6-7890-1234-56789abcdef0"
|
||||
name: "Admin"
|
||||
actions: ["create", "update", "delete"]
|
||||
access_type: "admin"
|
||||
- id: "c01ed106-e52d-4aa4-bed3-39f360177cfa"
|
||||
roles:
|
||||
- id: "b2c3d4e5-f678-9012-3456-789abcdef012"
|
||||
name: "Editor"
|
||||
actions: ["read", "update"]
|
||||
access_type: "write"
|
||||
|
||||
|
||||
AvailableActionsObj:
|
||||
type: object
|
||||
properties:
|
||||
available_actions:
|
||||
type: array
|
||||
description: List of all available actions.
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
[
|
||||
"read",
|
||||
"update",
|
||||
]
|
||||
|
||||
parameters:
|
||||
RoleName:
|
||||
name: roleName
|
||||
description: Role's name.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
example: "roleName"
|
||||
RoleID:
|
||||
name: roleID
|
||||
description: Role ID.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
example: "roleID"
|
||||
RoleIDQuery:
|
||||
name: role_id
|
||||
description: Filter by role ID.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
RoleNameQuery:
|
||||
name: role_name
|
||||
description: Filter by role name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: admin
|
||||
ActionsQuery:
|
||||
name: actions
|
||||
description: Filter by actions. Multiple actions can be specified separated by comma.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: "action1,action2"
|
||||
|
||||
requestBodies:
|
||||
CreateRoleReq:
|
||||
description: JSON- formatted object decsribing a new role to be created.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateRoleObj'
|
||||
|
||||
UpdateRoleReq:
|
||||
description: JSON- formatted object decsribing a role to be updated.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateRoleObj'
|
||||
|
||||
AddRoleActionsReq:
|
||||
description: JSON- formatted object decsribing an action to be added to a role.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleActionsObj'
|
||||
|
||||
AddRoleMembersReq:
|
||||
description: JSON- formatted object decsribing a member to be added to a role.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleMembersObj'
|
||||
|
||||
responses:
|
||||
CreateRoleRes:
|
||||
description: Role created successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NewRole'
|
||||
|
||||
ListRolesRes:
|
||||
description: Role retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RolesPage'
|
||||
|
||||
GetRoleRes:
|
||||
description: Role retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Role'
|
||||
|
||||
AddRoleActionsRes:
|
||||
description: Action added to role successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleActionsObj'
|
||||
|
||||
ListRoleActionsRes:
|
||||
description: Role actions retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleActionsObj'
|
||||
|
||||
AddRoleMembersRes:
|
||||
description: Member added to role successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleMembersObj'
|
||||
|
||||
ListRoleMembersRes:
|
||||
description: Role members retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoleMembersObj'
|
||||
|
||||
ListAvailableActionsRes:
|
||||
description: Available actions retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AvailableActionsObj'
|
||||
|
||||
|
||||
ListEntityMembersRes:
|
||||
description: Entity members retrieved successfully.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/EntityMembersObj'
|
||||
+1727
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
package mainflux
|
||||
|
||||
import "os"
|
||||
|
||||
// Env reads specified environment variable. If no value has been found,
|
||||
// fallback is returned.
|
||||
func Env(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user