Compare commits

..

156 Commits

Author SHA1 Message Date
pedro c4dff2afb6 devicehub entrypoint: put one worker 2024-04-03 17:59:33 +02:00
pedro a0b14461b3 docker: bugfix devicehub entrypoint
entrypoint was being put inside the working directory, which is
preserved

that made entrypoint "persistent", avoiding future updates

solution is to put it in the root (/), which is not preserved
2024-04-03 13:13:32 +02:00
pedro b607eedf5f adapt entrypoint to idhub trustchain pilot 2024-04-03 13:01:40 +02:00
Cayo Puigdefabregas 8b4f4b2c6e fix host 2024-04-02 22:03:50 +02:00
Cayo Puigdefabregas 2a7a10178c fix https 2024-04-02 13:24:55 +02:00
Cayo Puigdefabregas 2283f20ab2 fix access to table with schema and without 2024-03-27 17:32:57 +01:00
Cayo Puigdefabregas a4c7b2a744 add new flow for get credentials from wallet 2024-03-26 18:51:46 +01:00
Cayo Puigdefabregas da8d43f9f6 fix new dlt_keys structure 2024-03-26 18:25:06 +01:00
Cayo Puigdefabregas f0710e88ec first step 2024-03-26 17:52:58 +01:00
Cayo Puigdefabregas 55839a26ea fix did 2024-02-22 17:49:44 +01:00
Cayo Puigdefabregas 39f0300a28 fix register_user_dlt when there are more than one user 2024-02-22 11:57:05 +01:00
Cayo Puigdefabregas e5dbb09025 fix roles 2024-02-21 16:19:39 +01:00
Cayo Puigdefabregas c948b0bca5 update readme 2024-02-20 15:59:20 +01:00
Cayo Puigdefabregas cd440b9931 fix get role in register_user_dlt 2024-02-16 20:59:21 +01:00
Cayo Puigdefabregas 44b1a245b6 fix get role in register_user_dlt 2024-02-16 20:34:36 +01:00
Cayo Puigdefabregas f9ec594a0e update READMES 2024-02-16 20:18:17 +01:00
pedro 397e4978e2 docker refactor
- use latest tag
- use launcher (which builds and deploys the docker compose)
- small fixes
- updated README
2024-02-16 11:02:18 +01:00
Cayo Puigdefabregas 77dd11ea23 uncomment create user demo un initdatas 2024-02-16 10:51:56 +01:00
Cayo Puigdefabregas 2c039c0a12 comment create user demo un initdatas 2024-02-16 10:48:49 +01:00
Cayo Puigdefabregas 4d416f426c fix lot trade 2024-02-09 22:41:56 +01:00
Cayo Puigdefabregas 74fe50b6fb add action Recycled with dlt proof 2024-02-09 20:45:17 +01:00
Cayo Puigdefabregas 9fb2b1c94a add snapthot and dpp to action EWaste 2024-02-08 18:40:02 +01:00
Cayo Puigdefabregas 708dad64aa fix readme and env.example 2024-02-08 12:41:54 +01:00
Cayo Puigdefabregas b680a3574d add user_devicehub example 2024-02-08 12:36:03 +01:00
Cayo Puigdefabregas b6e9be306b fix READMES 2024-02-08 12:34:55 +01:00
Cayo Puigdefabregas 95700e7ba4 comment set rols for users 2024-02-05 14:54:00 +01:00
Cayo Puigdefabregas 5c02b424c1 fix version of ereuseapitest 2024-02-05 12:40:18 +01:00
Cayo Puigdefabregas 51e54450d8 fix get_manuals 2024-01-30 21:54:34 +01:00
Cayo Puigdefabregas 9100f315f9 fix config 2024-01-30 20:33:09 +01:00
Cayo Puigdefabregas 8b0d1f4b7d add env vars 2024-01-30 18:32:32 +01:00
Cayo Puigdefabregas 46bfef2585 remove engine_options 2024-01-30 18:29:45 +01:00
Cayo Puigdefabregas 660fe41b62 fix change max connections 2024-01-30 12:04:17 +01:00
Cayo Puigdefabregas e687cef4a3 fix docker 2024-01-29 19:46:53 +01:00
Cayo Puigdefabregas af338cfa4c fix 2024-01-29 18:53:06 +01:00
Cayo Puigdefabregas 72c8fac29e fix new api for iota 2024-01-29 18:49:02 +01:00
Cayo Puigdefabregas ca273285fe Merge branch 'new_dpp' into oidc4vp-new 2024-01-29 17:49:35 +01:00
Cayo Puigdefabregas 0197ddb4d1 fix new dict of dlt_secrets 2024-01-24 14:47:42 +01:00
Cayo Puigdefabregas 87e3c3e917 fix 2024-01-23 18:38:34 +01:00
Cayo Puigdefabregas 5aaa88971a not create user in docker init 2024-01-23 18:14:46 +01:00
Cayo Puigdefabregas 16ae2c08da add new register user in dlt 2024-01-23 18:11:40 +01:00
Cayo Puigdefabregas 6a75423532 add new register user in dlt 2024-01-23 18:11:40 +01:00
Cayo Puigdefabregas c9238e8e6a get ABAC_URL from config in user model 2024-01-23 18:11:40 +01:00
Cayo Puigdefabregas 53289b0dfc add ABAC_URL in config 2024-01-23 18:11:40 +01:00
pedro 14e37ef8c4 docker: IMPORT_SNAPSHOTS: change default to no 2024-01-23 12:56:26 +01:00
Cayo Puigdefabregas 1a5384e302 change url of iota explorer 2024-01-10 18:39:33 +01:00
Cayo Puigdefabregas d2b4de7c41 fix loop 2023-12-14 12:53:48 +01:00
Cayo Puigdefabregas fcc4b34424 fix check credentials in loop 2023-12-14 12:48:46 +01:00
Cayo Puigdefabregas 69d1873da4 fix now vcredential is a list 2023-12-14 10:19:39 +01:00
Cayo Puigdefabregas a33613b21d fix & in the url 2023-12-14 09:51:52 +01:00
Cayo Puigdefabregas 9721344244 fix presentation_definition_uri 2023-12-14 09:39:46 +01:00
Cayo Puigdefabregas 2806e7ab18 fix filter 2023-12-13 19:07:29 +01:00
Cayo Puigdefabregas 37fb688f77 fix login_required 2023-12-13 18:56:00 +01:00
Cayo Puigdefabregas 6c7395c26f fix primarykey 2023-12-13 18:49:52 +01:00
Cayo Puigdefabregas 18600af272 fix next_url 2023-12-13 10:39:00 +01:00
Cayo Puigdefabregas 278377090a flow for connect to wallet 2023-12-12 20:40:11 +01:00
Cayo Puigdefabregas fc7d7b4549 get roles from credential 2023-12-11 21:30:56 +01:00
Cayo Puigdefabregas 543aad813d try conenct to api iota for validation 2023-12-08 20:54:44 +01:00
Cayo Puigdefabregas 950cc59cae new endpoint 2023-12-06 14:03:49 +01:00
Cayo Puigdefabregas ada42f291a add abac datas in session 2023-11-06 16:48:03 +01:00
Cayo Puigdefabregas ab4ec523c3 add Iota did and attributes 2023-11-06 13:23:47 +01:00
pedro 20ee5ae411 docker: remove unneeded env var 2023-10-24 09:14:18 +02:00
pedro 57eb978dc4 docker-compose: upgrade images 2023-10-24 08:54:08 +02:00
pedro c6ec665865 env.example: new DEPLOYMENT var 2023-10-24 08:49:55 +02:00
pedro 5a0990f22a docker: new features regarding persistence
- added db persistence
- bugfix init_flagfile, as now its volume is persisted, it really does
  the configuration step when needed
- also added production deployment for the non dpp deployment
2023-10-24 08:43:27 +02:00
Cayo Puigdefabregas 0ad35de5d6 add new changes of ereuseapitest 2023-10-23 19:43:10 +02:00
Cayo Puigdefabregas aa966b5b93 change name of algorithm 2023-10-20 12:29:49 +02:00
Cayo Puigdefabregas bc3d3abcd7 add endpoint for check the proofs 2023-10-03 14:37:49 +02:00
pedro 9ca92f4c56 dc.yml: update docker images 2023-10-01 11:38:32 +02:00
Cayo Puigdefabregas 15d6851043 explain the new vars 2023-09-29 14:29:00 +02:00
pedro bcb4c69677 add optional dpp_module and snapshots
- dpp_module becomes optional
  - docker compose for simple devicehub and with the dpp module
  - changed logic in entrypoint so different parts are configured or
  - not depending on the new DPP_MODULE env var
- optional snapshots
  - new default directory for SNAPSHOTS_PATH
  - new env var IMPORT_SNAPSHOTS to optionally import the snapshots or not
- new Make file targets:
  - dc_up_devicehub: docker compose for simple devicehub
  - dc_up_devicehub_dpp: docker compose for devicehub with DPP_module
2023-09-29 10:51:48 +02:00
pedro b594022194 make API_RESOLVER more resilient (normalize urls) 2023-09-29 10:46:01 +02:00
pedro c226138ff2 bugfix dockerfile build: add app.py from example 2023-09-29 10:46:01 +02:00
Cayo Puigdefabregas 6c3831d103 fix readme 2023-09-29 10:05:35 +02:00
Cayo Puigdefabregas a7f5de96a5 fix text README 2023-09-29 10:03:42 +02:00
Cayo Puigdefabregas 594fe1483f changes readme 2023-09-28 18:31:18 +02:00
Cayo Puigdefabregas ece944ea3f change README 2023-09-28 17:57:56 +02:00
Cayo Puigdefabregas ac3d318fc9 change the examples files 2023-09-28 16:40:36 +02:00
Cayo Puigdefabregas 0181bd34ae changes on README.md 2023-09-28 16:37:02 +02:00
Cayo Puigdefabregas 5fa6f46acc clean example app.py 2023-09-28 13:15:46 +02:00
Cayo Puigdefabregas 4ba7bcc956 fix link in readme 2023-09-28 08:49:32 +02:00
cayop fde966ec13
Merge pull request #463 from eReuse/dpp_docker
add dockerization of devicehub dpp
2023-09-28 08:38:51 +02:00
Cayo Puigdefabregas cb0c7f1cb6 fix env example 2023-09-28 08:37:41 +02:00
Cayo Puigdefabregas 68c342ee18 fix readme 2023-09-28 08:27:05 +02:00
Cayo Puigdefabregas b614fad41f fix readme 2023-09-28 08:26:10 +02:00
Cayo Puigdefabregas 7e088eefc8 new readme 2023-09-28 08:24:03 +02:00
Cayo Puigdefabregas 82bf535915 new readme 2023-09-28 08:18:24 +02:00
Cayo Puigdefabregas 843324bd17 fix strip slash in domain 2023-09-28 08:17:47 +02:00
Cayo Puigdefabregas 740007b804 fix env.example 2023-09-27 08:03:09 +02:00
Cayo Puigdefabregas c8ab0a959e add Manufacturer DPP in templates 2023-09-26 17:32:42 +02:00
pedro dce2873158 update env.example: we need real reachable URLs
specially for the OIDC demo
2023-09-21 22:12:03 +02:00
pedro 2dc40e95fe docker-compose: update images 2023-09-21 21:40:29 +02:00
pedro 5a965e245e docker-compose: use unique users for each instance 2023-09-21 21:40:16 +02:00
pedro 6a58dcc68f refactor Makefile
- use ereuse project (to avoid confusion between devicehub project and
devicehub image)
- facilitate the docker image URL on the make docker_build
2023-09-21 21:37:37 +02:00
pedro 2c4b0006cc bugfix oidc client not working
the file gets created, but you need to wait some time to get data into it
2023-09-21 21:33:11 +02:00
pedro 7a85ebd8f8 client_id_config is not a global env var
then, move it to downcase to avoid confusion
2023-09-21 20:57:00 +02:00
pedro 9dec42bd05 docker-compose: update devicehub image 2023-09-21 19:07:14 +02:00
pedro 37069ff561 bugfix docker compose devicehub client id 2023-09-21 18:44:43 +02:00
pedro b423a53cfe automate OIDC setup for devicehub server & client 2023-09-21 18:44:43 +02:00
pedro 260ac90f86 reorder env vars in entrypoint for coherence 2023-09-21 18:44:43 +02:00
pedro f37800dcd3 docker: publish new image and put it in d-compose 2023-09-21 18:44:43 +02:00
pedro 907bf2dba0 add basic dockerization to devicehub dpp 2023-09-21 18:44:43 +02:00
Cayo Puigdefabregas 0b70f42daa fix json doble quotes 2023-09-21 18:40:21 +02:00
Cayo Puigdefabregas 8a7a9476fe fix command add contract oidc 2023-09-21 13:19:30 +02:00
Cayo Puigdefabregas 5069c793cf change result as json 2023-09-20 16:33:20 +02:00
Cayo Puigdefabregas 0f26bf63c6 add contract oidc command 2023-09-20 15:54:22 +02:00
Cayo Puigdefabregas bf3474e3db update README 2023-09-19 09:05:59 +02:00
Cayo Puigdefabregas 274c99db43 add context to json for only chassis 2023-09-18 16:35:30 +02:00
Cayo Puigdefabregas 2f250402e3 add context to json 2023-09-18 16:20:55 +02:00
Cayo Puigdefabregas 3f86242bfb add context to json 2023-09-18 16:05:10 +02:00
Cayo Puigdefabregas 5de416796e fix proof action 2023-09-18 11:41:58 +02:00
Cayo Puigdefabregas 0fb4fa5ba6 refactor 2023-08-22 10:41:48 +02:00
Cayo Puigdefabregas 33fc69013f fix reset password 2023-08-16 10:45:54 +02:00
Cayo Puigdefabregas d5f8b1ec75 render datas energy star in operator role 2023-08-10 16:44:20 +02:00
Cayo Puigdefabregas 5aee8f3f8f get energy star datas from manuals api 2023-08-10 16:43:45 +02:00
Cayo Puigdefabregas 9c2f22c77a remove pdbs 2023-08-10 16:43:03 +02:00
Cayo Puigdefabregas db706503fc fix printing messages 2023-08-03 17:32:50 +02:00
Cayo Puigdefabregas 11f3b7730a more info about the chekers for every step 2023-08-03 16:36:51 +02:00
Cayo Puigdefabregas 7857a85e8f add ignore files 2023-07-28 10:32:35 +02:00
Cayo Puigdefabregas ac02246ddc add check install 2023-07-28 10:15:05 +02:00
Cayo Puigdefabregas 33efa7ab75 fix remove files 2023-07-25 15:54:24 +02:00
Cayo Puigdefabregas 74789d66d1 add pip install 2023-07-25 13:18:01 +02:00
Cayo Puigdefabregas 1a107bb2db create snapshot_files directory 2023-07-25 13:04:01 +02:00
Cayo Puigdefabregas 0d2dd2fcb1 add input in snapshot command for select user 2023-07-25 09:49:29 +02:00
Cayo Puigdefabregas 7f449aa95c add new call to proofs 2023-07-24 19:23:26 +02:00
Cayo Puigdefabregas 4f2cfe5c47 remove files 2023-07-24 12:33:04 +02:00
Cayo Puigdefabregas 1b4159d58b fix format 2023-07-24 09:29:46 +02:00
Cayo Puigdefabregas 13d36f5650 upload snapshot from web client 2023-07-21 18:02:22 +02:00
Cayo Puigdefabregas 9ff20740dd get user from .env config 2023-07-21 17:21:08 +02:00
Cayo Puigdefabregas 0b0d9edaad add command upload snapshot 2023-07-21 17:20:15 +02:00
cayop 77f39ef78c
Update Definition-dpp.md
fix documentation
2023-07-19 11:34:58 +02:00
cayop 947deb45af
Update Definition-dpp.md 2023-07-19 11:32:48 +02:00
Cayo Puigdefabregas 7c91314d4a Document define what is a Dpp 2023-07-19 11:23:41 +02:00
Cayo Puigdefabregas 56b36ab244 add laer datas in operator template 2023-07-17 17:13:55 +02:00
Cayo Puigdefabregas 79cb5279e9 Merge branch 'dpp' into join-to-manuals 2023-07-13 17:18:35 +02:00
Cayo Puigdefabregas fdb4d90ab4 add chid of components 2023-07-11 16:39:04 +02:00
Cayo Puigdefabregas b5ae2b0629 add manuals in templates: 2023-07-07 16:57:36 +02:00
Cayo Puigdefabregas 748516edaf example of repair manuals 2023-07-06 17:49:54 +02:00
Cayo Puigdefabregas 5e3af04a8c Merge branch 'dpp' into join-to-manuals 2023-07-06 16:35:50 +02:00
Cayo Puigdefabregas 472d742db2 allow verify dpp 2023-07-06 12:54:18 +02:00
Cayo Puigdefabregas 36c61d49ff get manuals first step 2023-07-06 11:39:37 +02:00
Cayo Puigdefabregas 2f9a2edb44 make calls to manuals resouce 2023-07-05 11:54:58 +02:00
Cayo Puigdefabregas 8762705cb5 fix nonce 2023-06-23 14:29:08 +02:00
Cayo Puigdefabregas 0438cbb509 fix 2023-06-23 11:59:45 +02:00
Cayo Puigdefabregas e451668ff9 fix templates 2023-06-23 10:22:46 +02:00
Cayo Puigdefabregas 945c0d42f9 fix, phid_dpp instead of json_wb 2023-06-21 12:48:36 +02:00
Cayo Puigdefabregas 94ddc76e17 fix sequence in database 2023-06-21 12:47:41 +02:00
Cayo Puigdefabregas 453fa52963 role instead of rol 2023-06-21 12:32:44 +02:00
Cayo Puigdefabregas 73fa8a6d28 role instead of rol 2023-06-21 12:27:10 +02:00
Cayo Puigdefabregas a68784c94c env migrations 2023-06-16 18:20:11 +02:00
Cayo Puigdefabregas 311ca3ca51 mv did as a module and migrates files to dpp 2023-06-16 18:05:52 +02:00
Cayo Puigdefabregas 1f78c184b5 Merge branch 'testing' into dpp 2023-06-16 13:12:38 +02:00
Cayo Puigdefabregas 99f4c71ee1 fix test 2023-06-16 13:04:37 +02:00
Cayo Puigdefabregas a6684999a8 add dpp and oidc modules 2023-06-16 12:39:03 +02:00
Cayo Puigdefabregas 8f333e04ae Merge branch 'testing' into dpp 2023-06-16 11:55:41 +02:00
Cayo Puigdefabregas 57caf52c02 fix initial id 2023-06-16 11:55:21 +02:00
Cayo Puigdefabregas 52da9c99ba commands 2023-06-16 09:31:16 +02:00
91 changed files with 5216 additions and 347 deletions

10
.gitignore vendored
View File

@ -127,8 +127,16 @@ yarn.lock
# ESLint Report # ESLint Report
eslint_report.json eslint_report.json
modules/ # modules/
tmp/ tmp/
.env* .env*
bin/ bin/
env* env*
examples/create-db2.sh
package-lock.json
snapshots/
!examples/snapshots
modules/
# emacs
*~

43
Definition-dpp.md Normal file
View File

@ -0,0 +1,43 @@
# Definitions
* A dpp is two hash strings joined by the character ":"
We call the first chain chid and the second phid.
* The chid and phid are hash strings of certain values.
We call the set of these values Documents.
Here we define these values.
## Chid
The chid is the part of dpp that defines a device, be it a computer,
a hard drive, etc. The chid is the most important part of a dpp since
anyone who comes across a device should be able to play it.
The chid is made up of four values:
* type
* manufacturer
* model
* serial_number
type represents the device type according to the devicehub.
These values are always represented in lowercase.
These values have to be ordered and concatenated with the character "-"
So:
{type}-{manufacturer}-{model}-{serial_number}
For example:
```
harddrive-seagate-st500lt0121dg15-s3p9a81f
```
In computer types this combination is not perfect and **can lead to collisions**.
That is why we need a value that is reliable and comes from the manufacturer.
## Phid
The values of the phid do not have to be reproducible. For this reason, each inventory can establish its own values and its order as a document.
It is important that each inventory store the document in string so that it can reproduce exactly the document that was hashed. So a document can be verifiable.
In the case of the DeviceHub, we use as the chid document all the values that the Workbench collects that describe the hardware's own data.
These data change depending on the version of the Workbench used.

49
Makefile Normal file
View File

@ -0,0 +1,49 @@
project := dkr-dsg.ac.upc.edu/ereuse
branch := `git branch --show-current`
commit := `git log -1 --format=%h`
#tag := ${branch}__${commit}
tag := latest
# docker images
devicehub_image := ${project}/devicehub:${tag}
postgres_image := ${project}/postgres:${tag}
# 2. Create a virtual environment.
docker_build:
docker build -f docker/devicehub.Dockerfile -t ${devicehub_image} .
# DEBUG
#docker build -f docker/devicehub.Dockerfile -t ${devicehub_image} . --progress=plain --no-cache
docker build -f docker/postgres.Dockerfile -t ${postgres_image} .
# DEBUG
#docker build -f docker/postgres.Dockerfile -t ${postgres_image} . --progress=plain --no-cache
@printf "\n##########################\n"
@printf "\ndevicehub image: ${devicehub_image}\n"
@printf "postgres image: ${postgres_image}\n"
@printf "\ndocker images built\n"
@printf "\n##########################\n\n"
docker_publish:
docker push ${devicehub_image}
docker push ${postgres_image}
.PHONY: docker
docker:
$(MAKE) docker_build
$(MAKE) docker_publish
@printf "\ndocker images published\n"
# manage 2 kinds of deployments with docker compose
dc_up_devicehub:
docker compose -f docker-compose_devicehub.yml up || true
dc_down_devicehub:
docker compose -f docker-compose_devicehub.yml down -v || true
dc_up_devicehub_dpp:
docker compose -f docker-compose_devicehub-dpp.yml up || true
dc_down_devicehub_dpp:
docker compose -f docker-compose_devicehub-dpp.yml down -v || true

203
README.md
View File

@ -1,151 +1,122 @@
# Devicehub # Devicehub
Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org) Devicehub is a distributed IT Asset Management System focused on reusing digital devices, created under the [eReuse.org](https://www.ereuse.org) initiative.
This README explains how to install and use Devicehub. [The documentation](http://devicehub.ereuse.org) explains the concepts and the API. This README explains how to install and use Devicehub. [The documentation](http://devicehub.ereuse.org) explains the concepts, usage and the API it provides.
Devicehub is built with [Teal](https://github.com/ereuse/teal) and [Flask](http://flask.pocoo.org). Devicehub is built with [Teal](https://github.com/ereuse/teal) and [Flask](http://flask.pocoo.org).
Devicehub relies on the existence of an [API_DLT connector](https://gitlab.com/dsg-upc/ereuse-dpp) verifiable data registry service, where specific operations are recorded to keep an external track record (ledger).
# Installing # Installing
The requirements are: Please visit the [Manual Installation](README_MANUAL_INSTALLATION.md) instructions to understand the detailed steps to install it locally or deploy it on a server. However, we recommend the following Docker deployment process.
- Python 3.7.3 or higher. In debian 10 is `# apt install python3`. # Docker
- [PostgreSQL 11 or higher](https://www.postgresql.org/download/). There is a Docker compose file for an automated deployment. Two instances of DeviceHub will be deployed. The following steps describe how to run and use it.
- Weasyprint [dependencie](http://weasyprint.readthedocs.io/en/stable/install.html)
Install Devicehub with *pip*: `pip3 install -U -r requirements.txt -e .` 1. Download the sources:
```
# Running git clone https://github.com/eReuse/devicehub-teal.git -b oidc4vp
Create a PostgreSQL database called *devicehub* by running [create-db](examples/create-db.sh): cd devicehub-teal
- In Linux, execute the following two commands (adapt them to your distro):
1. `sudo su - postgres`.
2. `bash examples/create-db.sh devicehub dhub`, and password `ereuse`.
- In MacOS: `bash examples/create-db.sh devicehub dhub`, and password `ereuse`.
Configure project using environment file (you can use provided example as quickstart):
```bash
$ cp examples/env.example .env
``` ```
Using the `dh` tool for set up with one or multiple inventories. 2. If you want to initialise one of DeviceHub instances (running on port 5000) with sample device snapshots, copy it/them into that directory. e.g.
Create the tables in the database by executing: ```
cp snapshot01.json examples/snapshots/
```bash
$ export dhi=dbtest; dh inv add --common --name dbtest
``` ```
Finally, run the app: Otherwise, the device inventory of your DeviceHub instance will be empty and ready to add new devices. For that (no snapshot import), you need to change the var to 'n' in the **.env** file
```
```bash IMPORT_SNAPSHOTS='n'
$ export dhi=dbtest;dh run --debugger
``` ```
The error bdist_wheel can happen when you work with a *virtual environment*. To register new devices, the [workbench software](https://github.com/eReuse/workbench) can be run on a device to generate its hardware snapshot that can be uploaded to one of the two DeviceHub instance.
To fix it, install in the *virtual environment* wheel
package. `pip3 install wheel`
## Multiple instances 3. Setup the environment variables in the .env file. You can find one example in examples/env.example.
If you don't have any, you can copy that example and modify the basic vars
```
cp examples/env.example .env
```
You can use these parameters as default for a local test, but default values may not be suitable for an internet-exposed service for security reasons. However, these six variables need to be initialised:
```
API_DLT
API_DLT_TOKEN
API_RESOLVER
ABAC_TOKEN
ABAC_USER
ABAC_URL
SERVER_ID_FEDERATED
CLIENT_ID_FEDERATED
```
The first six values should come from an already operational [API_DLT connector](https://gitlab.com/dsg-upc/ereuse-dpp) service instance.
Devicehub can run as a single inventory or with multiple inventories, each inventory being an instance of the `devicehub`. To add a new inventory execute: For the last two values check [manual install step 9]('https://github.com/eReuse/devicehub-teal/blob/oidc4vp/README_MANUAL_INSTALLATION.md#installing') for more details.
```bash
$ export dhi=dbtest; dh inv add --name dbtest 4. Build and run the docker containers:
```
./launcher.sh
```
To stop these docker containers, you can use Ctl+C. You'll maintain the data and infrastructure state if you run "compose up" again.
On the terminal screen, you can follow the installation steps. If there are any problems, error messages will appear here. The appearance of several warnings is normal and can be ignored.
If the last line you see one text like this, *exited with code*:
```
devicehub-teal-devicehub-id-client-1 exited with code 1
```
means the installation failed.
If the deployment was end-to-end successful (two running Devicehub instances successfully connected to the DLT backend selected in the .env file), you can see this text in the last lines:
```
devicehub-teal-devicehub-id-client-1 | * Running on http://172.28.0.2:5000/ (Press CTRL+C to quit)
devicehub-teal-devicehub-id-server-1 | * Running on all addresses.
devicehub-teal-devicehub-id-server-1 | WARNING: This is a development server. Do not use it in a production deployment.
devicehub-teal-devicehub-id-server-1 | * Running on http://172.28.0.5:5000/ (Press CTRL+C to quit)
``` ```
Note: The `dh` command is like `flask`, but it allows you to create and delete instances, and interface to them directly. That means the two Devicehub instances are running in their containers, which can be reached as http://localhost:5000/ and http://localhost:5001/
Once the DeviceHub instances are running, you might want to register a user binding to the DLT with the following commands (here, it assumes you want to execute it on devicehub-id-client, you might also want to do it in devicehub-id-server). Change the variables accordingly
# Testing ```
FILE=my_users_devicehub.json
1. `git clone` this project. DOCKER_SERVICE=devicehub-id-server
2. Create a database for testing executing `create-db.sh` like the normal installation but changing the first parameter from `devicehub` to `dh_test`: `create-db.sh dh_test dhub` and password `ereuse`. docker compose cp /path/to/${FILE} ${DOCKER_SERVICE}:/tmp/
3. Execute at the root folder of the project `python3 setup.py test`. docker compose exec ${DOCKER_SERVICE} flask dlt_register_user /tmp/${FILE}
# Migrations
At this stage, migration files are created manually.
Set up the database:
```bash
$ sudo su - postgres
$ bash $PATH_TO_DEVIHUBTEAL/examples/create-db.sh devicehub dhub
``` ```
Initialize the database: **my_users_devicehub.json** is a custom file which is similar to the one provided in `examples/users_devicehub.json`
```bash 5. To shut down the services and remove the corresponding data, you can use:
$ export dhi=dbtest; dh inv add --common --name dbtest ```
docker compose down -v
``` ```
This command will create the schemas, tables in the specified database. If you want to enter a shell inside a **new instance of the container**:
Then we need to stamp the initial migration. ```
docker run -it --entrypoint= ${target_docker_image} bash
```bash
$ alembic stamp head
``` ```
If you want to enter a shell on an **already running container**:
This command will set the revision **fbb7e2a0cde0_initial** as our initial migration. ```
For more info in migration stamping please see https://alembic.sqlalchemy.org/en/latest/cookbook.html docker exec -it ${target_docker_image} bash
Whenever a change needed eg to create a new schema, alter an existing table, column or perform any
operation on tables, create a new revision file:
```bash
$ alembic revision -m "A table change"
``` ```
This command will create a new revision file with name `<revision_id>_a_table_change`. To know the valid value for ${target_docker_image} you can use:
Edit the generated file with the necessary operations to perform the migration: ```
docker ps
```bash
$ alembic edit <revision_id>
``` ```
Apply migrations using: 6. These are the details for use in this implementation:
```bash Devicehub with URL (http://localhost:5000) is the identity provider of OIDC and have a user defined in **.env** file with SERVER_ID_EMAIL_DEMO var.
$ alembic -x inventory=dbtest upgrade head
Devicehub with URL (http://localhost:5001) is the client identity of OIDC and have a user defined in **.env** file with SERVER_ID_EMAIL_DEMO var.
You can change these values in the *.env* file
7. If you want to use Workbench for these DeviceHub instances, you need to go to
``` ```
Then to go back to previous db version: http://localhost:5001/workbench/
```bash
$ alembic -x inventory=dbtest downgrade <revision_id>
``` ```
with the demo user and then download the settings and ISO files. Follow the instructions on the [help](https://help.usody.com/en/setup/setup-pendrive/) page.
To see a full list of migrations use
```bash
$ alembic history
```
# Upgrade a deployment
For upgrade an instance of devicehub you need to do:
```bash
$ cd $PATH_TO_DEVIHUBTEAL
$ source venv/bin/activate
$ git pull
$ alembic -x inventory=dbtest upgrade head
```
If all migrations pass successfully, then it is necessary restart the devicehub.
Normaly you can use a little script for restart.
```
# sh gunicorn_api.sh
```
## Generating the docs
1. `git clone` this project.
2. Install plantuml. In Debian 9 is `# apt install plantuml`.
3. Execute `pip3 install -e .[docs]` in the project root folder.
4. Go to `<project root folder>/docs` and execute `make html`. Repeat this step to generate new docs.
To auto-generate the docs do `pip3 install -e .[docs-auto]`, then execute, in the root folder of the project `sphinx-autobuild docs docs/_build/html`.

View File

@ -0,0 +1,187 @@
# Devicehub
Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org)
This README explains how to install and use Devicehub. [The documentation](http://devicehub.ereuse.org) explains the concepts and the API.
Devicehub is built with [Teal](https://github.com/ereuse/teal) and [Flask](http://flask.pocoo.org).
# Installing
The requirements are:
0. Required
- python3.9
- [PostgreSQL 11 or higher](https://www.postgresql.org/download/).
- Weasyprint [dependencie](http://weasyprint.readthedocs.io/en/stable/install.html)
1. Generate a clone of the repository.
```
git clone git@github.com:eReuse/devicehub-teal.git -b oidc4vp
cd devicehub-teal
```
2. Create a virtual environment and install Devicehub with *pip*.
```
python3.9 -m venv env
source env/bin/activate
sh examples/pip_install.sh
```
3. Create a PostgreSQL database called *devicehub* by running [create-db](examples/create-db.sh):
- In Linux, execute the following two commands (adapt them to your distro):
1. `sudo su - postgres`.
2. `bash examples/create-db.sh devicehub dhub`, and password `ereuse`.
- In MacOS: `bash examples/create-db.sh devicehub dhub`, and password `ereuse`.
Configure project using environment file (you can use provided example as quickstart):
```bash
$ cp examples/env.example .env
```
You can use these parameters as default for a local test, but default values may not be suitable for an internet-exposed service for security reasons. However, these six variables need to be initialized:
```
API_DLT
API_DLT_TOKEN
API_RESOLVER
ABAC_TOKEN
ABAC_USER
ABAC_URL
```
These values should come from an already operational [API_DLT connector](https://gitlab.com/dsg-upc/ereuse-dpp) service instance.
4. Running alembic from oidc module.
```
alembic -x inventory=dbtest upgrade head
```
5. Running alembic from oidc module.
```
cd ereuse_devicehub/modules/oidc
alembic -x inventory=dbtest upgrade head
```
6. Running alembic from dpp module.
```
cd ereuse_devicehub/modules/dpp/
alembic -x inventory=dbtest upgrade head
```
7. Add a suitable app.py file.
```
cp examples/app.py .
```
8. Generate a minimal data structure.
```
flask initdata
```
9. Add a new server to the 'api resolver' to be able to integrate it into the federation.
The domain name for this new server has to be unique. When installing two instances their domain name must differ: e.g. dpp.mydomain1.cxm, dpp.mydomain2.cxm.
If your domain is dpp.mydomain.cxm:
```
flask dlt_insert_members http://dpp.mydomain.cxm
```
modify the .env file as indicated in point 3.
Add the corresponding 'DH' in ID_FEDERATED.
example: ID_FEDERATED='DH10'
10. Do a rsync api resolve.
```
flask dlt_rsync_members
```
11. Register a new user in devicehub.
```
flask adduser email@example.org password
```
12. Register a new user to the DLT.
```
flask dlt_register_user examples/users_devicehub.json
```
You need define your users in the file **users_devicehub.json**
13. Finally, run the app:
```bash
$ flask run --debugger
```
The error bdist_wheel can happen when you work with a *virtual environment*.
To fix it, install in the *virtual environment* wheel
package. `pip3 install wheel`
# Testing
1. `git clone` this project.
2. Create a database for testing executing `create-db.sh` like the normal installation but changing the first parameter from `devicehub` to `dh_test`: `create-db.sh dh_test dhub` and password `ereuse`.
3. Execute at the root folder of the project `python3 setup.py test`.
# Upgrade a deployment
For upgrade an instance of devicehub you need to do:
```bash
$ cd $PATH_TO_DEVIHUBTEAL
$ source venv/bin/activate
$ git pull
$ alembic -x inventory=dbtest upgrade head
```
If all migrations pass successfully, then it is necessary restart the devicehub.
Normaly you can use a little script for restart or run.
```
# systemctl stop gunicorn_devicehub.socket
# systemctl stop gunicorn_devicehub.service
# systemctl start gunicorn_devicehub.service
```
# OpenId Connect:
We want to interconnect two devicehub instances already installed. One has a set of devices (OIDC client), the other has a set of users (OIDC identity server). Let's assume their domains are: dpp.mydomain1.cxm, dpp.mydomain2.cxm
20. In order to connect the two devicehub instances, it is necessary:
* 20.1. Register a user in the devicehub instance acting as OIDC identity server.
* 20.2. Fill in the openid connect form.
* 20.3. Add in the OIDC client inventory the data of client_id, client_secret.
For 20.1. This can be achieved on the terminal on the devicehub instance acting as OIDC identity server.
```
flask adduser email@example.org password
```
* 20.2. This is an example of how to fill in the form.
In the web interface of the OIDC identity service, click on the profile of the just added user, select "My Profile" and click on "OpenID Connect":
Then we can go to the "OpenID Connect" panel and fill out the form:
The important thing about this form is:
* "Client URL" The URL of the OIDC Client instance, as registered in point 12. dpp.mydomain1.cxm in our example.
* "Allowed Scope" has to have these three words:
```
openid profile rols
```
* "Redirect URIs" it has to be the URL that was put in "Client URL" plus "/allow_code"
* "Allowed Grant Types" has to be "authorization_code"
* "Allowed Response Types" has to be "code"
* "Token Endpoint Auth Method" has to be "Client Secret Basic"
After clicking on "Submit" the "OpenID Connect" tab of the user profile should now include details for "client_id" and "client_secret".
* 20.3. In the OIDC client inventory run: (in our example: url_domain is dpp.mydomain2.cxm, client_id and client_secret as resulting from the previous step)
```
flask add_client_oidc url_domain client_id client_secret
```
After this step, both servers must be connected. Opening one DPP page on dpp.mydomain1.cxm (OIDC Client) the user can choose to authenticate using dpp.mydomain2.cxm (OIDC Server).
## Generating the docs
1. `git clone` this project.
2. Install plantuml. In Debian 9 is `# apt install plantuml`.
3. Execute `pip3 install -e .[docs]` in the project root folder.
4. Go to `<project root folder>/docs` and execute `make html`. Repeat this step to generate new docs.
To auto-generate the docs do `pip3 install -e .[docs-auto]`, then execute, in the root folder of the project `sphinx-autobuild docs docs/_build/html`.

1
docker-compose.yml Symbolic link
View File

@ -0,0 +1 @@
docker-compose_devicehub-dpp.yml

View File

@ -0,0 +1,103 @@
version: "3.9"
services:
devicehub-id-server:
init: true
image: dkr-dsg.ac.upc.edu/ereuse/devicehub:latest
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=postgres-id-server
- DB_DATABASE=${DB_DATABASE}
- HOST=${HOST}
- EMAIL_DEMO=${SERVER_ID_EMAIL_DEMO}
- PASSWORD_DEMO=${PASSWORD_DEMO}
- JWT_PASS=${JWT_PASS}
- SECRET_KEY=${SECRET_KEY}
- API_DLT=${API_DLT}
- API_RESOLVER=${API_RESOLVER}
- API_DLT_TOKEN=${API_DLT_TOKEN}
- DEVICEHUB_HOST=${SERVER_ID_DEVICEHUB_HOST}
- ID_FEDERATED=${SERVER_ID_FEDERATED}
- URL_MANUALS=${URL_MANUALS}
- ID_SERVICE=${SERVER_ID_SERVICE}
- AUTHORIZED_CLIENT_URL=${CLIENT_ID_DEVICEHUB_HOST}
- DPP_MODULE=y
- IMPORT_SNAPSHOTS=${IMPORT_SNAPSHOTS}
ports:
- 5000:5000
volumes:
- ${SNAPSHOTS_PATH:-./examples/snapshots}:/mnt/snapshots:ro
- shared:/shared:rw
- app_id_server:/opt/devicehub:rw
postgres-id-server:
image: dkr-dsg.ac.upc.edu/ereuse/postgres:latest
# 4. To create the database.
# 5. Give permissions to the corresponding users in the database.
# extra src https://github.com/docker-library/docs/blob/master/postgres/README.md#environment-variables
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_USER=${DB_USER}
- POSTGRES_DB=${DB_DATABASE}
# DEBUG
#ports:
# - 5432:5432
# TODO persistence
#volumes:
# - pg_data:/var/lib/postgresql/data
devicehub-id-client:
init: true
image: dkr-dsg.ac.upc.edu/ereuse/devicehub:latest
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=postgres-id-client
- DB_DATABASE=${DB_DATABASE}
- HOST=${HOST}
- EMAIL_DEMO=${CLIENT_ID_EMAIL_DEMO}
- PASSWORD_DEMO=${PASSWORD_DEMO}
- JWT_PASS=${JWT_PASS}
- SECRET_KEY=${SECRET_KEY}
- API_DLT=${API_DLT}
- API_RESOLVER=${API_RESOLVER}
- API_DLT_TOKEN=${API_DLT_TOKEN}
- DEVICEHUB_HOST=${CLIENT_ID_DEVICEHUB_HOST}
- SERVER_ID_HOST=${SERVER_ID_DEVICEHUB_HOST}
- ID_FEDERATED=${CLIENT_ID_FEDERATED}
- URL_MANUALS=${URL_MANUALS}
- ID_SERVICE=${CLIENT_ID_SERVICE}
- DPP_MODULE=y
- IMPORT_SNAPSHOTS=${IMPORT_SNAPSHOTS}
ports:
- 5001:5000
volumes:
- ${SNAPSHOTS_PATH:-./examples/snapshots}:/mnt/snapshots:ro
- shared:/shared:ro
- app_id_client:/opt/devicehub:rw
postgres-id-client:
image: dkr-dsg.ac.upc.edu/ereuse/postgres:latest
# 4. To create the database.
# 5. Give permissions to the corresponding users in the database.
# extra src https://github.com/docker-library/docs/blob/master/postgres/README.md#environment-variables
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_USER=${DB_USER}
- POSTGRES_DB=${DB_DATABASE}
# DEBUG
#ports:
# - 5432:5432
# TODO persistence
#volumes:
# - pg_data:/var/lib/postgresql/data
# TODO https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/
#nginx
volumes:
shared:
app_id_client:
app_id_server:

View File

@ -0,0 +1,54 @@
version: "3.9"
services:
devicehub:
init: true
image: dkr-dsg.ac.upc.edu/ereuse/devicehub:dpp__c6ec6658
environment:
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=postgres
- DB_DATABASE=${DB_DATABASE}
- HOST=${HOST}
- EMAIL_DEMO=${EMAIL_DEMO}
- PASSWORD_DEMO=${PASSWORD_DEMO}
- JWT_PASS=${JWT_PASS}
- SECRET_KEY=${SECRET_KEY}
- DEVICEHUB_HOST=${DEVICEHUB_HOST}
- URL_MANUALS=${URL_MANUALS}
- DPP_MODULE=n
- IMPORT_SNAPSHOTS=${IMPORT_SNAPSHOTS}
- DEPLOYMENT=${DEPLOYMENT}
ports:
- 5000:5000
volumes:
- ${SNAPSHOTS_PATH:-./examples/snapshots}:/mnt/snapshots:ro
- shared:/shared:rw
- app:/opt/devicehub:rw
postgres:
image: dkr-dsg.ac.upc.edu/ereuse/postgres:dpp__c6ec6658
# 4. To create the database.
# 5. Give permissions to the corresponding users in the database.
# extra src https://github.com/docker-library/docs/blob/master/postgres/README.md#environment-variables
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_USER=${DB_USER}
- POSTGRES_DB=${DB_DATABASE}
volumes:
- pg_data:/var/lib/postgresql/data
# DEBUG
#ports:
# - 5432:5432
nginx:
image: nginx
ports:
- 8080:8080
volumes:
- ./docker/nginx-devicehub.nginx.conf:/etc/nginx/nginx.conf:ro
volumes:
shared:
pg_data:
app:

View File

@ -0,0 +1,32 @@
FROM debian:bullseye-slim
RUN apt update && apt-get install --no-install-recommends -y \
python3-minimal \
python3-pip \
python-is-python3 \
python3-psycopg2 \
python3-dev \
libpq-dev \
build-essential \
libpangocairo-1.0-0 \
curl \
jq \
time \
netcat
WORKDIR /opt/devicehub
# this is exactly the same as examples/pip_install.sh except the last command
# to improve the docker layer builds, it has been separated
RUN pip install --upgrade pip
RUN pip install alembic==1.8.1 anytree==2.8.0 apispec==0.39.0 atomicwrites==1.4.0 blinker==1.5 boltons==23.0.0 cairocffi==1.4.0 cairosvg==2.5.2 certifi==2022.9.24 cffi==1.15.1 charset-normalizer==2.0.12 click==6.7 click-spinner==0.1.8 colorama==0.3.9 colour==0.1.5 cssselect2==0.7.0 defusedxml==0.7.1 et-xmlfile==1.1.0 flask==1.0.2 flask-cors==3.0.10 flask-login==0.5.0 flask-sqlalchemy==2.5.1 flask-weasyprint==0.4 flask-wtf==1.0.0 hashids==1.2.0 html5lib==1.1 idna==3.4 inflection==0.5.1 itsdangerous==2.0.1 jinja2==3.0.3 mako==1.2.3 markupsafe==2.1.1 marshmallow==3.0.0b11 marshmallow-enum==1.4.1 more-itertools==8.12.0 numpy==1.22.0 odfpy==1.4.1 openpyxl==3.0.10 pandas==1.3.5 passlib==1.7.1 phonenumbers==8.9.11 pillow==9.2.0 pint==0.9 psycopg2-binary==2.8.3 py-dmidecode==0.1.0 pycparser==2.21 pyjwt==2.4.0 pyphen==0.13.0 python-dateutil==2.7.3 python-decouple==3.3 python-dotenv==0.14.0 python-editor==1.0.4 python-stdnum==1.9 pytz==2022.2.1 pyyaml==5.4 requests==2.27.1 requests-mock==1.5.2 requests-toolbelt==0.9.1 six==1.16.0 sortedcontainers==2.1.0 sqlalchemy==1.3.24 sqlalchemy-citext==1.3.post0 sqlalchemy-utils==0.33.11 tinycss2==1.1.1 tqdm==4.32.2 urllib3==1.26.12 weasyprint==44 webargs==5.5.3 webencodings==0.5.1 werkzeug==2.0.3 wtforms==3.0.1 xlrd==2.0.1 cryptography==39.0.1 Authlib==1.2.1 gunicorn==21.2.0
RUN pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
COPY . .
# this operation might be overriding inside container another app.py you would have
COPY examples/app.py .
RUN pip install -e .
COPY docker/devicehub.entrypoint.sh /
ENTRYPOINT sh /devicehub.entrypoint.sh

View File

@ -0,0 +1,12 @@
.git
.env
# TODO need to comment it to copy the entrypoint
#docker
Makefile
# Emacs backup files
*~
.\#*
# Vim swap files
*.swp
*.swo

228
docker/devicehub.entrypoint.sh Executable file
View File

@ -0,0 +1,228 @@
#!/bin/sh
set -e
set -u
# DEBUG
set -x
# 3. Generate an environment .env file.
gen_env_vars() {
CONFIG_OIDC="${CONFIG_OIDC:-y}"
# specific dpp env vars
if [ "${DPP_MODULE}" = 'y' ]; then
dpp_env_vars="$(cat <<END
API_DLT='${API_DLT}'
API_DLT_TOKEN='${API_DLT_TOKEN}'
API_RESOLVER='${API_RESOLVER}'
ID_FEDERATED='${ID_FEDERATED}'
END
)"
fi
# generate config using env vars from docker
cat > .env <<END
${dpp_env_vars:-}
DB_USER='${DB_USER}'
DB_PASSWORD='${DB_PASSWORD}'
DB_HOST='${DB_HOST}'
DB_DATABASE='${DB_DATABASE}'
URL_MANUALS='${URL_MANUALS}'
HOST='${HOST}'
SCHEMA='dbtest'
DB_SCHEMA='dbtest'
EMAIL_DEMO='${EMAIL_DEMO}'
PASSWORD_DEMO='${PASSWORD_DEMO}'
JWT_PASS=${JWT_PASS}
SECRET_KEY=${SECRET_KEY}
END
}
wait_for_postgres() {
# old one was
#sleep 4
default_postgres_port=5432
# thanks https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/
while ! nc -z ${DB_HOST} ${default_postgres_port}; do
sleep 0.5
done
}
init_data() {
# 7. Run alembic of the project.
alembic -x inventory=dbtest upgrade head
# 8. Running alembic from oidc module.y
cd ereuse_devicehub/modules/oidc
alembic -x inventory=dbtest upgrade head
cd -
# 9. Running alembic from dpp module.
cd ereuse_devicehub/modules/dpp/
alembic -x inventory=dbtest upgrade head
cd -
# 11. Generate a minimal data structure.
# TODO it has some errors (?)
flask initdata || true
if [ "${EREUSE_PILOT:-}" = 'y' ]; then
flask dlt_register_user /opt/devicehub/users_devicehub.json || true
fi
}
big_error() {
local message="${@}"
echo "###############################################" >&2
echo "# ERROR: ${message}" >&2
echo "###############################################" >&2
exit 1
}
handle_federated_id() {
# devicehub host and id federated checker
# //getAll queries are not accepted by this service, so we remove them
EXPECTED_ID_FEDERATED="$(curl -s "${API_RESOLVER%/}/getAll" \
| jq -r '.url | to_entries | .[] | select(.value == "'"${DEVICEHUB_HOST}"'") | .key' \
| head -n 1)"
# if is a new DEVICEHUB_HOST, then register it
if [ -z "${EXPECTED_ID_FEDERATED}" ]; then
# TODO better docker compose run command
cmd="docker compose run --entrypoint= devicehub flask dlt_insert_members ${DEVICEHUB_HOST}"
big_error "No FEDERATED ID maybe you should run \`${cmd}\`"
fi
# if not new DEVICEHUB_HOST, then check consistency
# if there is already an ID in the DLT, it should match with my internal ID
if [ ! "${EXPECTED_ID_FEDERATED}" = "${ID_FEDERATED}" ]; then
big_error "ID_FEDERATED should be ${EXPECTED_ID_FEDERATED} instead of ${ID_FEDERATED}"
fi
# not needed, but reserved
# EXPECTED_DEVICEHUB_HOST="$(curl -s "${API_RESOLVER%/}/getAll" \
# | jq -r '.url | to_entries | .[] | select(.key == "'"${ID_FEDERATED}"'") | .value' \
# | head -n 1)"
# if [ ! "${EXPECTED_DEVICEHUB_HOST}" = "${DEVICEHUB_HOST}" ]; then
# big_error "ERROR: DEVICEHUB_HOST should be ${EXPECTED_DEVICEHUB_HOST} instead of ${DEVICEHUB_HOST}"
# fi
}
config_oidc() {
# TODO test allowing more than 1 client
if [ "${ID_SERVICE}" = "server_id" ]; then
client_description="client identity from docker compose demo"
# in AUTHORIZED_CLIENT_URL we remove anything before ://
flask add_contract_oidc \
"${EMAIL_DEMO}" \
"${client_description}" \
"${AUTHORIZED_CLIENT_URL}" \
> /shared/client_id_${AUTHORIZED_CLIENT_URL#*://}
elif [ "${ID_SERVICE}" = "client_id" ]; then
# in DEVICEHUB_HOST we remove anything before ://
client_id_config="/shared/client_id_${DEVICEHUB_HOST#*://}"
client_id=
client_secret=
# wait that the file generated by the server_id is readable
while true; do
if [ -f "${client_id_config}" ]; then
client_id="$(cat "${client_id_config}" | jq -r '.client_id')"
client_secret="$(cat "${client_id_config}" | jq -r '.client_secret')"
if [ "${client_id}" ] && [ "${client_secret}" ]; then
break
fi
fi
sleep 1
done
flask add_client_oidc \
"${SERVER_ID_HOST}" \
"${client_id}" \
"${client_secret}"
else
big_error "Something went wrong ${ID_SERVICE} is not server_id nor client_id"
fi
}
config_dpp_part1() {
# 12. Add a new server to the 'api resolver'
handle_federated_id
# 13. Do a rsync api resolve
flask dlt_rsync_members
# 14. Register a new user to the DLT
#flask dlt_register_user "${EMAIL_DEMO}" ${PASSWORD_DEMO} Operator
}
config_phase() {
init_flagfile='docker__already_configured'
if [ ! -f "${init_flagfile}" ]; then
# 7, 8, 9, 11
init_data
if [ "${DPP_MODULE}" = 'y' ]; then
# 12, 13, 14
config_dpp_part1
fi
# non DL user (only for the inventory)
# flask adduser user2@dhub.com ${PASSWORD_DEMO}
# # 15. Add inventory snapshots for user "${EMAIL_DEMO}".
if [ "${IMPORT_SNAPSHOTS}" = 'y' ]; then
mkdir -p ereuse_devicehub/commands/snapshot_files
cp /mnt/snapshots/snapshot*.json ereuse_devicehub/commands/snapshot_files/
/usr/bin/time flask snapshot "${EMAIL_DEMO}" ${PASSWORD_DEMO}
fi
if [ "${CONFIG_OIDC}" = 'y' ]; then
# 16.
# commented because this fails with wrong DLT credentials
#flask check_install "${EMAIL_DEMO}" "${PASSWORD_DEMO}"
# 20. config server or client ID
config_oidc
fi
# remain next command as the last operation for this if conditional
touch "${init_flagfile}"
fi
}
main() {
gen_env_vars
wait_for_postgres
config_phase
# 17. Use gunicorn
# thanks https://akira3030.github.io/formacion/articulos/python-flask-gunicorn-docker.html
if [ "${DEPLOYMENT:-}" = "PROD" ]; then
# TODO workers 1 because we have a shared secret in RAM
gunicorn --access-logfile - --error-logfile - --workers 1 -b :5000 app:app
else
# run development server
FLASK_DEBUG=1 flask run --host=0.0.0.0 --port 5000
fi
# DEBUG
#sleep infinity
}
main "${@}"

View File

@ -0,0 +1,32 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
#upstream socket_backend {
# server unix:/socket/gunicorn.sock fail_timeout=0;
#}
server {
listen 8080;
listen [::]:8080;
#server_name devicehub.example.org;
location / {
# TODO env var on proxy_pass
proxy_pass http://devicehub:5000/;
proxy_set_header Host $http_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_redirect off;
}
}
}

View File

@ -0,0 +1,8 @@
FROM postgres:15.4-bookworm
# this is the latest in 2023-09-14_13-01-38
#FROM postgres:latest
# Add a SQL script that will be executed upon container startup
COPY docker/postgres.setupdb.sql /docker-entrypoint-initdb.d/
EXPOSE 5432

View File

@ -0,0 +1,5 @@
-- 6. Create the necessary extensions.
CREATE EXTENSION pgcrypto SCHEMA public;
CREATE EXTENSION ltree SCHEMA public;
CREATE EXTENSION citext SCHEMA public;
CREATE EXTENSION pg_trgm SCHEMA public;

View File

@ -0,0 +1,125 @@
"""This command is used for up one snapshot."""
import json
import click
from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.user.models import User
class CheckInstall:
"""Command.
This command check if the installation was ok and the
integration with the api of DLT was ok too.
"""
def __init__(self, app) -> None:
"""Init function."""
super().__init__()
self.app = app
self.schema = app.config.get('DB_SCHEMA')
self.app.cli.command('check_install', short_help='Upload snapshots.')(self.run)
@click.argument('email')
@click.argument('password')
def run(self, email, password):
"""Run command."""
self.email = email
self.password = password
self.OKGREEN = '\033[92m'
# self.WARNING = '\033[93m'
self.FAIL = '\033[91m'
self.ENDC = '\033[0m'
print("\n")
try:
self.check_user()
self.check_snapshot()
except Exception:
txt = "There was an Error in the installation!"
print("\n" + self.FAIL + txt + self.ENDC)
return
txt = "The installation is OK!"
print("\n" + self.OKGREEN + txt + self.ENDC)
def check_user(self):
"""Get datamodel of user."""
self.user = User.query.filter_by(email=self.email).first()
txt = "Register user to the DLT "
try:
assert self.user.api_keys_dlt is not None
token_dlt = self.user.get_dlt_keys(self.password)
assert token_dlt.get('data', {}).get('eth_pub_key') is not None
except Exception:
self.print_fail(txt)
raise (txt)
self.print_ok(txt)
api_token = token_dlt.get('data', {}).get('api_token')
txt = "Register user roles in the DLT "
try:
rols = self.user.get_rols(api_token)
assert self.user.rols_dlt is not None
assert self.user.rols_dlt != []
assert self.user.rols_dlt == json.dumps([x for x, y in rols])
except Exception:
self.print_fail(txt)
raise (txt)
self.print_ok(txt)
def check_snapshot(self):
self.snapshot = Snapshot.query.filter_by(author=self.user).first()
if not self.snapshot:
txt = "Impossible register snapshot "
self.print_fail(txt)
raise (txt)
self.device = self.snapshot.device
txt = "Generate DPP "
try:
assert self.device.chid is not None
assert self.snapshot.json_wb is not None
assert self.snapshot.phid_dpp is not None
except Exception:
self.print_fail(txt)
raise (txt)
self.print_ok(txt)
txt = "Register DPP in the DLT "
try:
assert len(self.device.dpps) > 0
dpp = self.device.dpps[0]
assert type(dpp.timestamp) == int
assert dpp in self.snapshot.dpp
assert dpp.documentId == str(self.snapshot.uuid)
# if 'Device already exists' in DLT before
# device.proofs == 0
# Snapshot.proof == 1 [erase]
# if Device is new in DLT before
# device.proofs == 1
# Snapshot.proof == 1 or 2 [Register, erase]
assert len(self.device.proofs) in [0, 1]
assert len(self.snapshot.proofs) in [0, 1, 2]
except Exception:
self.print_fail(txt)
raise (txt)
self.print_ok(txt)
def print_ok(self, msg):
print(msg + self.OKGREEN + " OK!" + self.ENDC)
def print_fail(self, msg):
print(msg + self.FAIL + " FAIL!" + self.ENDC)

View File

@ -1,6 +1,7 @@
from uuid import uuid4 from uuid import uuid4
from boltons.urlutils import URL from boltons.urlutils import URL
from decouple import config
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.agent.models import Person from ereuse_devicehub.resources.agent.models import Person
@ -13,6 +14,9 @@ class InitDatas:
super().__init__() super().__init__()
self.app = app self.app = app
self.schema = app.config.get('DB_SCHEMA') self.schema = app.config.get('DB_SCHEMA')
self.email = config('EMAIL_DEMO')
self.name = self.email.split('@')[0] if self.email else None
self.password = config('PASSWORD_DEMO')
self.app.cli.command( self.app.cli.command(
'initdata', short_help='Save a minimum structure of datas.' 'initdata', short_help='Save a minimum structure of datas.'
)(self.run) )(self.run)
@ -29,12 +33,9 @@ class InitDatas:
db.session.add(inv) db.session.add(inv)
db.session.commit() db.session.commit()
email = 'user@dhub.com' if self.email:
password = '1234' user = User(email=self.email, password=self.password)
name = 'user' user.individuals.add(Person(name=self.name))
db.session.add(user)
user = User(email=email, password=password) db.session.commit()
user.individuals.add(Person(name=name))
db.session.add(user)
db.session.commit()

View File

@ -0,0 +1,103 @@
"""This command is used for up one snapshot."""
import json
# from uuid import uuid4
from io import BytesIO
from os import listdir
from os import remove as remove_file
from os.path import isfile, join
from pathlib import Path
import click
from flask.testing import FlaskClient
from flask_wtf.csrf import generate_csrf
from ereuse_devicehub.resources.user.models import User
class UploadSnapshots:
"""Command.
This command allow upload all snapshots than exist
in the directory snapshots_upload.
If this snapshot exist replace it.
"""
def __init__(self, app) -> None:
"""Init function."""
super().__init__()
self.app = app
self.schema = app.config.get('DB_SCHEMA')
self.app.cli.command('snapshot', short_help='Upload snapshots.')(self.run)
@click.argument('email')
@click.argument('password')
def run(self, email, password=None):
"""Run command."""
self.email = email
self.password = password
self.json_wb = None
self.onlyfiles = []
self.get_user()
self.get_files()
for f in self.onlyfiles:
self.file_snapshot = f
self.open_snapshot()
self.build_snapshot()
self.remove_files()
def get_user(self):
"""Get datamodel of user."""
self.user = User.query.filter_by(email=self.email).one()
self.client = FlaskClient(self.app, use_cookies=True)
self.client.get('/login/')
data = {
'email': self.email,
'password': self.password,
'remember': False,
'csrf_token': generate_csrf(),
}
self.client.post('/login/', data=data, follow_redirects=True)
def remove_files(self):
"""Open snapshot file."""
for f in self.onlyfiles:
remove_file(Path(__file__).parent.joinpath('snapshot_files').joinpath(f))
def open_snapshot(self):
"""Open snapshot file."""
with Path(__file__).parent.joinpath('snapshot_files').joinpath(
self.file_snapshot,
).open() as file_snapshot:
self.json_wb = json.loads(file_snapshot.read())
b_snapshot = bytes(json.dumps(self.json_wb), 'utf-8')
self.file_snap = (BytesIO(b_snapshot), self.file_snapshot)
def build_snapshot(self):
"""Build the devices of snapshot."""
uri = '/inventory/upload-snapshot/'
if not self.json_wb:
return
self.client.get(uri)
data = {
'snapshot': self.file_snap,
'csrf_token': generate_csrf(),
}
self.client.post(uri, data=data, content_type="multipart/form-data")
def get_files(self):
"""Read snaoshot_files dir."""
mypath = Path(__file__).parent.joinpath('snapshot_files')
for f in listdir(mypath):
if not isfile(join(mypath, f)):
continue
if not f[-5:] == ".json":
continue
self.onlyfiles.append(f)

View File

@ -52,12 +52,20 @@ class DevicehubConfig(Config):
DB_HOST = config('DB_HOST', 'localhost') DB_HOST = config('DB_HOST', 'localhost')
DB_DATABASE = config('DB_DATABASE', 'devicehub') DB_DATABASE = config('DB_DATABASE', 'devicehub')
DB_SCHEMA = config('DB_SCHEMA', 'dbtest') DB_SCHEMA = config('DB_SCHEMA', 'dbtest')
SQLALCHEMY_DATABASE_URI = 'postgresql://{user}:{pw}@{host}/{db}'.format( SQLALCHEMY_DATABASE_URI = 'postgresql://{user}:{pw}@{host}/{db}'.format(
user=DB_USER, user=DB_USER,
pw=DB_PASSWORD, pw=DB_PASSWORD,
host=DB_HOST, host=DB_HOST,
db=DB_DATABASE, db=DB_DATABASE,
) # type: str ) # type: str
SQLALCHEMY_POOL_SIZE = int(config("SQLALCHEMY_POOL_SIZE", 10))
SQLALCHEMY_MAX_OVERFLOW = int(config("SQLALCHEMY_MAX_OVERFLOW", 20))
SQLALCHEMY_TRACK_MODIFICATIONS = bool(config("SQLALCHEMY_TRACK_MODIFICATIONS", False))
SQLALCHEMY_POOL_TIMEOUT = int(config("SQLALCHEMY_POOL_TIMEOUT", 0))
SQLALCHEMY_POOL_RECYCLE = int(config("SQLALCHEMY_POOL_RECYCLE", 3600))
SCHEMA = config('SCHEMA', 'dbtest') SCHEMA = config('SCHEMA', 'dbtest')
HOST = config('HOST', 'localhost') HOST = config('HOST', 'localhost')
API_HOST = config('API_HOST', 'localhost') API_HOST = config('API_HOST', 'localhost')
@ -98,9 +106,19 @@ class DevicehubConfig(Config):
API_DLT = config('API_DLT', None) API_DLT = config('API_DLT', None)
API_DLT_TOKEN = config('API_DLT_TOKEN', None) API_DLT_TOKEN = config('API_DLT_TOKEN', None)
ID_FEDERATED = config('ID_FEDERATED', None) ID_FEDERATED = config('ID_FEDERATED', None)
URL_MANUALS = config('URL_MANUALS', None)
ABAC_TOKEN = config('ABAC_TOKEN', None)
ABAC_COOKIE = config('ABAC_COOKIE', None)
ABAC_URL = config('ABAC_URL', None)
VERIFY_URL = config('VERIFY_URL', None)
"""Definition of oauth jwt details.""" """Definition of oauth jwt details."""
OAUTH2_JWT_ENABLED = config('OAUTH2_JWT_ENABLED', False) OAUTH2_JWT_ENABLED = config('OAUTH2_JWT_ENABLED', False)
OAUTH2_JWT_ISS = config('OAUTH2_JWT_ISS', '') OAUTH2_JWT_ISS = config('OAUTH2_JWT_ISS', '')
OAUTH2_JWT_KEY = config('OAUTH2_JWT_KEY', None) OAUTH2_JWT_KEY = config('OAUTH2_JWT_KEY', None)
OAUTH2_JWT_ALG = config('OAUTH2_JWT_ALG', 'HS256') OAUTH2_JWT_ALG = config('OAUTH2_JWT_ALG', 'HS256')
if API_DLT:
API_DLT = API_DLT.strip("/")
WALLET_INX_EBSI_PLUGIN_TOKEN = config('WALLET_INX_EBSI_PLUGIN_TOKEN', None)
WALLET_INX_EBSI_PLUGIN_URL = config('WALLET_INX_EBSI_PLUGIN_URL', None)

View File

@ -70,6 +70,8 @@ def create_view(name, selectable):
return table return table
db = SQLAlchemy(session_options={'autoflush': False}) db = SQLAlchemy(
session_options={'autoflush': False},
)
f = db.func f = db.func
exp = expression exp = expression

View File

@ -13,7 +13,9 @@ import ereuse_devicehub.ereuse_utils.cli
from ereuse_devicehub.auth import Auth from ereuse_devicehub.auth import Auth
from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.client import Client, UserClient
from ereuse_devicehub.commands.adduser import AddUser from ereuse_devicehub.commands.adduser import AddUser
from ereuse_devicehub.commands.check_install import CheckInstall
from ereuse_devicehub.commands.initdatas import InitDatas from ereuse_devicehub.commands.initdatas import InitDatas
from ereuse_devicehub.commands.snapshots import UploadSnapshots
# from ereuse_devicehub.commands.reports import Report # from ereuse_devicehub.commands.reports import Report
from ereuse_devicehub.commands.users import GetToken from ereuse_devicehub.commands.users import GetToken
@ -29,7 +31,7 @@ from ereuse_devicehub.teal.teal import Teal
from ereuse_devicehub.templating import Environment from ereuse_devicehub.templating import Environment
try: try:
from ereuse_devicehub.modules.commands.sync_dlt import GetMembers from ereuse_devicehub.modules.oidc.commands.sync_dlt import GetMembers
except Exception: except Exception:
GetMembers = None GetMembers = None
@ -44,10 +46,20 @@ except Exception:
AddMember = None AddMember = None
try: try:
from ereuse_devicehub.modules.oidc.commands.add_member import AddClientOidc from ereuse_devicehub.modules.oidc.commands.client_member import AddClientOidc
except Exception: except Exception:
AddClientOidc = None AddClientOidc = None
try:
from ereuse_devicehub.modules.oidc.commands.insert_member_in_dlt import InsertMember
except Exception:
InsertMembe = None
try:
from ereuse_devicehub.modules.oidc.commands.add_contract_oidc import AddContractOidc
except Exception:
AddContractOidc = None
class Devicehub(Teal): class Devicehub(Teal):
test_client_class = Client test_client_class = Client
@ -97,15 +109,22 @@ class Devicehub(Teal):
self.get_token = GetToken(self) self.get_token = GetToken(self)
self.initdata = InitDatas(self) self.initdata = InitDatas(self)
self.adduser = AddUser(self) self.adduser = AddUser(self)
self.uploadsnapshots = UploadSnapshots(self)
self.checkinstall = CheckInstall(self)
if GetMembers: if GetMembers:
self.get_members = GetMembers(self) self.get_members = GetMembers(self)
if RegisterUserDlt: if RegisterUserDlt:
self.register_user_dlt = RegisterUserDlt(self) self.dlt_register_user = RegisterUserDlt(self)
if AddMember: if AddMember:
self.register_user_dlt = AddMember(self) self.dlt_insert_members = AddMember(self)
if AddClientOidc: if AddClientOidc:
self.register_user_dlt = AddClientOidc(self) self.add_client_oidc = AddClientOidc(self)
if InsertMember:
self.dlt_insert_members = InsertMember(self)
if AddContractOidc:
self.add_contract_oidc = AddContractOidc(self)
@self.cli.group( @self.cli.group(
short_help='Inventory management.', short_help='Inventory management.',

View File

@ -70,10 +70,14 @@ class LoginForm(FlaskForm):
self.form_errors.append(self.error_messages['inactive']) self.form_errors.append(self.error_messages['inactive'])
if 'dpp' in app.blueprints.keys(): if 'dpp' in app.blueprints.keys():
token_dlt = ( dlt_keys = user.get_dlt_keys(
user.get_dlt_keys(self.password.data).get('data', {}).get('api_token') self.password.data
) ).get('data', {})
token_dlt = dlt_keys.get('api_token')
eth_pub_key = dlt_keys.get('eth_pub_key')
session['token_dlt'] = token_dlt session['token_dlt'] = token_dlt
session['eth_pub_key'] = eth_pub_key
session['rols'] = user.get_rols() session['rols'] = user.get_rols()
return user.is_active return user.is_active
@ -111,12 +115,14 @@ class PasswordForm(FlaskForm):
return True return True
def save(self, commit=True): def save(self, commit=True):
if 'dpp' not in app.blueprints.keys(): if 'dpp' in app.blueprints.keys():
keys_dlt = g.user.get_dlt_keys(self.password.data) keys_dlt = g.user.get_dlt_keys(self.password.data)
g.user.reset_dlt_keys(self.newpassword.data, keys_dlt) g.user.reset_dlt_keys(self.newpassword.data, keys_dlt)
token_dlt = ( token_dlt = (
g.user.get_dlt_keys(self.password.data).get('data', {}).get('api_token') g.user.get_dlt_keys(self.newpassword.data)
.get('data', {})
.get('api_token')
) )
session['token_dlt'] = token_dlt session['token_dlt'] = token_dlt
@ -129,7 +135,6 @@ class PasswordForm(FlaskForm):
class SanitizationEntityForm(FlaskForm): class SanitizationEntityForm(FlaskForm):
logo = URLField( logo = URLField(
'Logo', 'Logo',
[validators.Optional(), validators.URL()], [validators.Optional(), validators.URL()],

View File

@ -42,6 +42,8 @@ from ereuse_devicehub.parser.models import PlaceholdersLog, SnapshotsLog
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
from ereuse_devicehub.parser.schemas import Snapshot_lite from ereuse_devicehub.parser.schemas import Snapshot_lite
from ereuse_devicehub.resources.action.models import Snapshot, Trade from ereuse_devicehub.resources.action.models import Snapshot, Trade
from ereuse_devicehub.resources.action.schemas import EWaste as EWasteSchema
from ereuse_devicehub.resources.action.schemas import Recycled as RecycledSchema
from ereuse_devicehub.resources.action.schemas import Snapshot as SnapshotSchema from ereuse_devicehub.resources.action.schemas import Snapshot as SnapshotSchema
from ereuse_devicehub.resources.action.views.snapshot import ( from ereuse_devicehub.resources.action.views.snapshot import (
SnapshotMixin, SnapshotMixin,
@ -840,6 +842,9 @@ class ActionFormMixin(FlaskForm):
if not self._devices: if not self._devices:
return False return False
if len(devices) > 1 and self.type.data == 'EWaste':
return False
return True return True
def generic_validation(self, extra_validators=None): def generic_validation(self, extra_validators=None):
@ -856,6 +861,17 @@ class ActionFormMixin(FlaskForm):
self.populate_obj(self.instance) self.populate_obj(self.instance)
db.session.add(self.instance) db.session.add(self.instance)
if self.instance.type == 'EWaste':
ewaste = EWasteSchema().dump(self.instance)
doc = "{}".format(ewaste)
self.instance.register_proof(doc)
if self.instance.type == 'Recycled':
recycled = RecycledSchema().dump(self.instance)
doc = "{}".format(recycled)
self.instance.register_proof(doc)
db.session.commit() db.session.commit()
self.devices.data = devices self.devices.data = devices
@ -1212,7 +1228,6 @@ class TradeForm(ActionFormMixin):
or email_to == email_from or email_to == email_from
or g.user.email not in [email_from, email_to] or g.user.email not in [email_from, email_to]
): ):
errors = ["If you want confirm, you need a correct email"] errors = ["If you want confirm, you need a correct email"]
self.user_to.errors = errors self.user_to.errors = errors
self.user_from.errors = errors self.user_from.errors = errors
@ -1918,7 +1933,6 @@ class UploadPlaceholderForm(FlaskForm):
return True return True
def save(self, commit=True): def save(self, commit=True):
for device, placeholder_log in self.placeholders: for device, placeholder_log in self.placeholders:
db.session.add(device) db.session.add(device)
db.session.add(placeholder_log) db.session.add(placeholder_log)
@ -1947,7 +1961,6 @@ class EditPlaceholderForm(FlaskForm):
return True return True
def save(self, commit=True): def save(self, commit=True):
for device in self.placeholders: for device in self.placeholders:
db.session.add(device) db.session.add(device)

View File

@ -1,7 +1,7 @@
"""sanitization """sanitization
Revision ID: 4f33137586dd Revision ID: 4f33137586dd
Revises: 8334535d56fa Revises: 93daff872771
Create Date: 2023-02-13 18:01:00.092527 Create Date: 2023-02-13 18:01:00.092527
""" """
@ -14,7 +14,7 @@ from ereuse_devicehub import teal
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '4f33137586dd' revision = '4f33137586dd'
down_revision = '8334535d56fa' down_revision = '93daff872771'
branch_labels = None branch_labels = None
depends_on = None depends_on = None

View File

@ -1,7 +1,7 @@
"""add vendor family in device """add vendor family in device
Revision ID: 564952310b17 Revision ID: 564952310b17
Revises: 4b7f77f121bf Revises: af038a8a388c
Create Date: 2022-11-14 13:12:22.916848 Create Date: 2022-11-14 13:12:22.916848
""" """
@ -11,7 +11,7 @@ from alembic import context, op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '564952310b17' revision = '564952310b17'
down_revision = '4b7f77f121bf' down_revision = 'af038a8a388c'
branch_labels = None branch_labels = None
depends_on = None depends_on = None

View File

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Device {{ device_real.dhid }} - Usody</title>
<meta content="" name="description">
<meta content="" name="keywords">
<!-- Favicons -->
<link href="{{ url_for('static', filename='img/favicon.png') }}" rel="icon">
<link href="{{ url_for('static', filename='img/apple-touch-icon.png') }}" rel="apple-touch-icon">
<!-- Google Fonts -->
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
<!-- JS Files -->
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
<!-- Vendor CSS Files -->
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}" rel="stylesheet">
<!-- Template Main CSS File -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/devicehub.css') }}" rel="stylesheet">
<!-- =======================================================
* Template Name: NiceAdmin - v2.2.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
======================================================== -->
</head>
<body>
<main>
<section class="container mt-3">
<div class="row">
<div class="col">
<nav class="header-nav ms-auto">
<ul class="d-flex align-items-right">
<li class="nav-item">
{% if not rols and user.is_anonymous %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
{% else %}
<button class="btn btn-primary" id="buttonRol" data-bs-toggle="modal" data-bs-target="#rolsModal">Select your role</button>
<a class="btn btn-primary" href="{{ url_for('core.logout') }}?next={{ path }}">Logout</a>
{% endif %}
</li>
</ul>
{% if rol %}
<br />Current Role: {{ rol }}
{% endif %}
</nav>
<div class="col-xl-12">
<div class="card">
<div class="card-body">
<h3 class="nav-link mt-5" style="color: #993365">{{ device_real.type }} - {{ device_real.verbose_name }}</h3>
<div class="row">
<div class="col-12">
<h5 class="card-title">Details</h5>
{% if manuals.details %}
<div class="row">
<div class="col">
{% if manuals.details.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
{% endif %}
</div>
<div class="col">
{% if manuals.details.image %}
<img style="width: 100px;" src="{{ manuals.details.image }}" />
{% endif %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col">
Type
</div>
<div class="col">
{{ device_real.type or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Manufacturer
</div>
<div class="col">
{{ device_real.manufacturer and device_real.manufacturer.upper() or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Model
</div>
<div class="col">
{{ device_real.model or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Device Identifier (CHID):
</div>
<div class="col">
<a href="{{ url_for('did.did', id_dpp=device_abstract.chid) }}">{{ device_abstract.chid }}</a>
</div>
</div>
<div class="row">
<div class="col">
Manufacturer DPP:
</div>
<div class="col">
</div>
</div>
<div class="row">
<div class="col">
Usody Identifier (DHID)
</div>
<div class="col">
{{ device_real.dhid }}
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h5 class="card-title">Components</h5>
<div class="row">
<div class="col">
<ul>
{% for component in components %}
{% if component.type == "Processor" %}
<li>
<strong>Processor</strong>: {{ component.manufacturer or '- not detected -' }} {{ component.model or '- not detected -'}}
</li>
{% endif %}
{% endfor %}
{% for component in components %}
{% if component.type in ['HardDrive', 'SolidStateDrive'] %}
<li>
<strong>{{ component.type }}</strong>:
{% if component.size %}{{ component.size/1000 }}GB{% else %} - not detected - {% endif %}
</li>
{% endif %}
{% endfor %}
{% for component in components %}
{% if component.type == 'RamModule' %}
<li>
<strong>Ram</strong>:
{% if component.size %}{{ component.size }}MB{% else %} - not detected - {% endif %}
</li>
{% endif %}
{% endfor %}
{% for component in components %}
{% if component.type == 'SoundCard' %}
<li>
<strong>Sound</strong>: {{ component.manufacturer or '- not detected -' }} {{ component.model or '- not detected -'}}
</li>
{% endif %}
{% endfor %}
{% for component in components %}
{% if component.type == 'NetworkAdapter' %}
<li>
<strong>Network</strong>: {{ component.manufacturer or '- not detected -' }} {{ component.model or '- not detected -'}}
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- ======= Footer ======= -->
<div class="container">
<div class="row">
<div class="col">
<footer class="footer">
<div class="copyright">
&copy; Copyright <strong><span>Usody</span></strong>. All Rights Reserved
</div>
<div class="credits">
<a href="https://help.usody.com/en/" target="_blank">Help</a> |
<a href="https://www.usody.com/legal/privacy-policy" target="_blank">Privacy</a> |
<a href="https://www.usody.com/legal/terms" target="_blank">Terms</a>
</div>
<div class="credits">
DeviceHub
</div>
</footer><!-- End Footer -->
</div>
</div>
</div>
{% if user.is_anonymous and not rols %}
<div class="modal fade" id="validateModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Validate as <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<a class="btn btn-primary" type="button"
href="{{ url_for('core.login') }}?next={{ path }}">
User of system
</a>
{% if oidc %}
<br />
<a class="btn btn-primary mt-3" type="button" href="{{ url_for('oidc.login_other_inventory') }}?next={{ path }}">
Use a wallet
</a>
{% endif %}
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
{% else %}
<div class="modal fade" id="rolsModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ path }}" method="get">
<div class="modal-header">
<h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<select name="rol">
{% for k, v in rols %}
<option value="{{ k }}" {% if v==rol %}selected=selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Send" />
</div>
</form>
</div>
</div>
</div>
{% endif %}
</body>
<!-- Custom Code -->
{% if rols and not rol %}
<script>
$(document).ready(() => {
$("#buttonRol").click();
});
</script>
{% endif %}
</html>

View File

@ -52,20 +52,20 @@
{% if not rols and user.is_anonymous %} {% if not rols and user.is_anonymous %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button> <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
{% else %} {% else %}
<button class="btn btn-primary" id="buttonRol" data-bs-toggle="modal" data-bs-target="#rolsModal">Select your rol</button> <button class="btn btn-primary" id="buttonRol" data-bs-toggle="modal" data-bs-target="#rolsModal">Select your role</button>
<a class="btn btn-primary" href="{{ url_for('core.logout') }}?next={{ path }}">Logout</a> <a class="btn btn-primary" href="{{ url_for('core.logout') }}?next={{ path }}">Logout</a>
{% endif %} {% endif %}
</li> </li>
</ul> </ul>
{% if rol %} {% if rol %}
<br />Actual Rol: {{ rol }} <br />Current Role: {{ rol }}
{% endif %} {% endif %}
</nav> </nav>
<div class="col-xl-12"> <div class="col-xl-12">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h3 class="nav-link mt-5" style="color: #007bc0;">{{ device_real.type }} - {{ device_real.verbose_name }}</h3> <h3 class="nav-link mt-5" style="color: #993365">{{ device_real.type }} - {{ device_real.verbose_name }}</h3>
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h5 class="card-title">Basic</h5> <h5 class="card-title">Basic</h5>
@ -264,7 +264,7 @@
<form action="{{ path }}" method="get"> <form action="{{ path }}" method="get">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Select your Rol <span id="title-action"></span></h5> <h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>

View File

@ -0,0 +1,585 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Device {{ device_real.dhid }} - Usody</title>
<meta content="" name="description">
<meta content="" name="keywords">
<!-- Favicons -->
<link href="{{ url_for('static', filename='img/favicon.png') }}" rel="icon">
<link href="{{ url_for('static', filename='img/apple-touch-icon.png') }}" rel="apple-touch-icon">
<!-- Google Fonts -->
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
<!-- JS Files -->
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
<!-- Vendor CSS Files -->
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}" rel="stylesheet">
<!-- Template Main CSS File -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/devicehub.css') }}" rel="stylesheet">
<!-- =======================================================
* Template Name: NiceAdmin - v2.2.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
======================================================== -->
</head>
<body>
<main>
<section class="container mt-3">
<div class="row">
<div class="col">
<nav class="header-nav ms-auto">
<ul class="d-flex align-items-right">
<li class="nav-item">
{% if not rols and user.is_anonymous %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
{% else %}
<button class="btn btn-primary" id="buttonRol" data-bs-toggle="modal" data-bs-target="#rolsModal">Select your role</button>
<a class="btn btn-primary" href="{{ url_for('core.logout') }}?next={{ path }}">Logout</a>
{% endif %}
</li>
</ul>
{% if rol %}
<br />Current Role: {{ rol }}
{% endif %}
</nav>
<div class="col-xl-12">
<div class="card">
<div class="card-body">
<h3 class="nav-link mt-5" style="color: #993365">{{ device_real.type }} - {{ device_real.verbose_name }}</h3>
<div class="row">
<div class="col-12">
<h5 class="card-title">Details</h5>
{% if manuals.details %}
<div class="row">
<div class="col">
{% if manuals.details.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
{% endif %}
</div>
<div class="col">
{% if manuals.details.image %}
<img style="width: 100px;" src="{{ manuals.details.image }}" />
{% endif %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col">
Type
</div>
<div class="col">
{{ device_real.type or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Manufacturer
</div>
<div class="col">
{{ device_real.manufacturer or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Model
</div>
<div class="col">
{{ device_real.model or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Part Number
</div>
<div class="col">
{{ device_real.part_number or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Serial Number
</div>
<div class="col">
{{ device_abstract.serial_number and device_abstract.serial_number.upper() or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Usody Identifier (DHID)
</div>
<div class="col">
{{ device_real.dhid }}
</div>
</div>
<div class="row">
<div class="col">
Inventory Identifier (PHID)
</div>
<div class="col">
{{ device_real.phid() }}
</div>
</div>
<div class="row">
<div class="col">
Device Identifier (CHID):
</div>
<div class="col">
<a href="{{ url_for('did.did', id_dpp=device_abstract.chid) }}">{{ device_abstract.chid|truncate(20, True, '...') }}</a>
</div>
</div>
<div class="row">
<div class="col">
Manufacturer DPP:
</div>
<div class="col">
</div>
</div>
<div class="row">
<div class="col">
Last Digital Passport (Last Dpp):
</div>
<div class="col">
{% if last_dpp %}
<a href="{{ url_for('did.did', id_dpp=last_dpp.key) }}">{{ last_dpp.key|truncate(20, True, '...') }}</a>
{% else %}
- not detected -
{% endif %}
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-6">
<h5 class="card-title">Status</h5>
<div class="row">
<div class="col">
<div class="label"><b>Physical</b></div>
<div>{{ device_real.physical_status and device.physical_status.type or '- not status -' }}</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="label"><b>Lifecycle</b></div>
<div>{{ device_real.status and device_real.status.type or '- not status -' }}</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="label"><b>Allocation</b></div>
<div>
{% if device_real.allocated %}
Allocated
{% else %}
Not allocated
{% endif %}
</div>
</div>
</div>
</div>
<div class="col-6">
{% if manuals.icecat %}
<h5 class="card-title">Icecat data sheet</h5>
<div class="row">
<div class="col-12 list-group-item d-flex align-items-center">
{% if manuals.details.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
{% endif %}
{% if manuals.details.image %}
<img style="max-width: 100px; margin-right: 15px;" src="{{ manuals.details.image }}" />
{% endif %}
{% if manuals.details.pdf %}
<a href="{{ manuals.details.pdf }}" target="_blank">{{ manuals.details.title }}</a><br />
{% else %}
{{ manuals.details.title }}<br />
{% endif %}
</div>
<div class="col-12 accordion-item">
<h5 class="card-title accordion-header">
<button class="accordion-button collapsed" data-bs-target="#manuals-icecat" type="button"
data-bs-toggle="collapse" aria-expanded="false">
More examples
</button>
</h5>
<div id="manuals-icecat" class="row accordion-collapse collapse">
<div class="accordion-body">
{% for m in manuals.icecat %}
<div class="list-group-item d-flex align-items-center">
{% if m.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ m.logo }}" />
{% endif %}
{% if m.pdf %}
<a href="{{ m.pdf }}" target="_blank">{{ m.title }}</a><br />
{% else %}
{{ m.title }}<br />
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="row mt-3">
<div class="col-6">
<h5 class="card-title">Components</h5>
<div class="row">
{% if components %}
<div class="list-group col">
{% for component in components|sort(attribute='type') %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ component.type }}</h5>
<small class="text-muted">{{ component.created.strftime('%H:%M %d-%m-%Y') }}</small>
</div>
<p class="mb-1">
Manufacturer: {{ component.manufacturer or '- not detected -' }}<br />
Model: {{ component.model or '- not detected -' }}<br />
Serial: {{ component.serial_number and component.serial_number.upper() or '- not detected -' }}
</p>
<small class="text-muted">
{% if component.type in ['RamModule', 'HardDrive', 'SolidStateDrive'] %}
{{ component.size }}MB
{% endif %}
</small>
</div>
{% endfor %}
</div>
{% else %}
<div class="list-group col">
<div class="list-group-item">
- not detected -
</div>
</div>
{% endif %}
</div>
</div>
<div class="col-6">
<h5 class="card-title">Repair history</h5>
<div class="row">
<div class="list-group col">
{% for action in placeholder.actions %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ action.type }} {{ action.severity }}
<small class="text-muted">{{ action.created.strftime('%H:%M %d-%m-%Y') }}</small>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% if manuals.laer %}
<div class="row mt-3">
<div class="col-12">
<h5 class="card-title">Recycled Content</h5>
<div class="row mb-3">
<div class="col-sm-2">
Metal
</div>
<div class="col-sm-10">
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: {{ manuals.laer.0.metal }}%"
aria-valuenow="{{ manuals.laer.0.metal }}"
aria-valuemin="0"
aria-valuemax="100">{{ manuals.laer.0.metal }}%
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-2">
Plastic post Consumer
</div>
<div class="col-sm-10">
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: {{ manuals.laer.0.plastic_post_consumer }}%"
aria-valuenow="{{ manuals.laer.0.plastic_post_consumer }}"
aria-valuemin="0"
aria-valuemax="100">{{ manuals.laer.0.plastic_post_consumer }}%
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-2">
Plastic post Industry
</div>
<div class="col-sm-10">
<div class="progress">
<div class="progress-bar"
role="progressbar"
style="width: {{ manuals.laer.0.plastic_post_industry }}%"
aria-valuenow="{{ manuals.laer.0.plastic_post_industry }}"
aria-valuemin="0"
aria-valuemax="100">{{ manuals.laer.0.plastic_post_industry }}%
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if manuals.energystar %}
<div class="row mt-3">
<div class="col-12">
<h5 class="card-title">Energy spent</h5>
{% if manuals.energystar.long_idle_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption when inactivity power function is activated (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.long_idle_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.short_idle_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption when inactivity power function is not activated (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.short_idle_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.sleep_mode_watts %}
<div class="row mb-3">
<div class="col-sm-10">
sleep_mode_watts
Consumption when computer goes into sleep mode (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.sleep_mode_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.off_mode_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption when the computer is off (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.off_mode_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.tec_allowance_kwh %}
<div class="row mb-3">
<div class="col-sm-10">
Power allocation for normal operation (kwh)
</div>
<div class="col-sm-2">
{{ manuals.energystar.tec_allowance_kwh }}
</div>
</div>
{% endif %}
{% if manuals.energystar.tec_of_model_kwh %}
<div class="row mb-3">
<div class="col-sm-10">
Consumption of the model configuration (kwh)
</div>
<div class="col-sm-2">
{{ manuals.energystar.tec_of_model_kwh }}
</div>
</div>
{% endif %}
{% if manuals.energystar.tec_requirement_kwh %}
<div class="row mb-3">
<div class="col-sm-10">
Energy allowance provided (kwh)
</div>
<div class="col-sm-2">
{{ manuals.energystar.tec_requirement_kwh }}
</div>
</div>
{% endif %}
{% if manuals.energystar.work_off_mode_watts %}
<div class="row mb-3">
<div class="col-sm-10">
The lowest power mode which cannot be switched off (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.work_off_mode_watts }}
</div>
</div>
{% endif %}
{% if manuals.energystar.work_weighted_power_of_model_watts %}
<div class="row mb-3">
<div class="col-sm-10">
Weighted energy consumption from all its states (watts)
</div>
<div class="col-sm-2">
{{ manuals.energystar.work_weighted_power_of_model_watts }}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if manuals.ifixit %}
<div class="row">
<div class="col-12 accordion-item">
<h5 class="card-title accordion-header">
<button class="accordion-button collapsed" data-bs-target="#manuals-repair" type="button"
data-bs-toggle="collapse" aria-expanded="false">
Repair manuals
</button>
</h5>
<div id="manuals-repair" class="row accordion-collapse collapse">
<div class="list-group col">
{% for m in manuals.ifixit %}
<div class="list-group-item d-flex align-items-center">
{% if m.image %}
<img style="max-width: 100px; margin-right: 15px;" src="{{ m.image }}" />
{% endif %}
{% if m.url %}
<a href="{{ m.url }}" target="_blank">{{ m.title }}</a><br />
{% else %}
{{ m.title }}<br />
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- ======= Footer ======= -->
<div class="container">
<div class="row">
<div class="col">
<footer class="footer">
<div class="copyright">
&copy; Copyright <strong><span>Usody</span></strong>. All Rights Reserved
</div>
<div class="credits">
<a href="https://help.usody.com/en/" target="_blank">Help</a> |
<a href="https://www.usody.com/legal/privacy-policy" target="_blank">Privacy</a> |
<a href="https://www.usody.com/legal/terms" target="_blank">Terms</a>
</div>
<div class="credits">
DeviceHub
</div>
</footer><!-- End Footer -->
</div>
</div>
</div>
{% if user.is_anonymous and not rols %}
<div class="modal fade" id="validateModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Validate as <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<a class="btn btn-primary" type="button"
href="{{ url_for('core.login') }}?next={{ path }}">
User of system
</a>
{% if oidc %}
<br />
<a class="btn btn-primary mt-3" type="button" href="{{ url_for('oidc.login_other_inventory') }}?next={{ path }}">
User of other inventory
</a>
{% endif %}
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
{% else %}
<div class="modal fade" id="rolsModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ path }}" method="get">
<div class="modal-header">
<h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<select name="rol">
{% for k, v in rols %}
<option value="{{ k }}" {% if v==rol %}selected=selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Send" />
</div>
</form>
</div>
</div>
</div>
{% endif %}
</body>
<!-- Custom Code -->
{% if not user.is_anonymous and not rol %}
<script>
$(document).ready(() => {
$("#buttonRol").click();
});
</script>
{% endif %}
</html>

View File

@ -0,0 +1,341 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Device {{ device_real.dhid }} - Usody</title>
<meta content="" name="description">
<meta content="" name="keywords">
<!-- Favicons -->
<link href="{{ url_for('static', filename='img/favicon.png') }}" rel="icon">
<link href="{{ url_for('static', filename='img/apple-touch-icon.png') }}" rel="apple-touch-icon">
<!-- Google Fonts -->
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
<!-- JS Files -->
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
<!-- Vendor CSS Files -->
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/bootstrap-icons/bootstrap-icons.css') }}" rel="stylesheet">
<!-- Template Main CSS File -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/devicehub.css') }}" rel="stylesheet">
<!-- =======================================================
* Template Name: NiceAdmin - v2.2.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
======================================================== -->
</head>
<body>
<main>
<section class="container mt-3">
<div class="row">
<div class="col">
<nav class="header-nav ms-auto">
<ul class="d-flex align-items-right">
<li class="nav-item">
{% if not rols and user.is_anonymous %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#validateModal">Validate</button>
{% else %}
<button class="btn btn-primary" id="buttonRol" data-bs-toggle="modal" data-bs-target="#rolsModal">Select your role</button>
<a class="btn btn-primary" href="{{ url_for('core.logout') }}?next={{ path }}">Logout</a>
{% endif %}
</li>
</ul>
{% if rol %}
<br />Current Role: {{ rol }}
{% endif %}
</nav>
<div class="col-xl-12">
<div class="card">
<div class="card-body">
<h3 class="nav-link mt-5" style="color: #993365">{{ device_real.type }} - {{ device_real.verbose_name }}</h3>
<div class="row">
<div class="col-12">
<h5 class="card-title">Details</h5>
{% if manuals.details %}
<div class="row">
<div class="col">
{% if manuals.details.logo %}
<img style="max-width: 50px; margin-right: 15px;" src="{{ manuals.details.logo }}" />
{% endif %}
</div>
<div class="col">
{% if manuals.details.image %}
<img style="width: 100px;" src="{{ manuals.details.image }}" />
{% endif %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col">
Type
</div>
<div class="col">
{{ device_real.type or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Manufacturer
</div>
<div class="col">
{{ device_real.manufacturer or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Model
</div>
<div class="col">
{{ device_real.model or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Part Number
</div>
<div class="col">
{{ device_real.part_number or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Serial Number
</div>
<div class="col">
{{ device_abstract.serial_number and device_abstract.serial_number.upper() or '- not detected -' }}
</div>
</div>
<div class="row">
<div class="col">
Usody Identifier (DHID)
</div>
<div class="col">
{{ device_real.dhid }}
</div>
</div>
<div class="row">
<div class="col">
Inventory Identifier (PHID)
</div>
<div class="col">
{{ device_real.phid() }}
</div>
</div>
<div class="row">
<div class="col">
Device Identifier (CHID):
</div>
<div class="col">
<a href="{{ url_for('did.did', id_dpp=device_abstract.chid) }}"><small class="text-muted">{{ device_abstract.chid }}</small></a>
</div>
</div>
<div class="row">
<div class="col">
Manufacturer DPP:
</div>
<div class="col">
</div>
</div>
{% if last_dpp %}
<div class="row">
<div class="col">
Last Digital Passport (Last Dpp):
</div>
</div>
<div class="row">
<div class="col">
<a href="{{ url_for('did.did', id_dpp=last_dpp.key) }}"><small class="text-muted">{{ last_dpp.key }}</small></a>
</div>
</div>
{% else %}
<div class="row">
<div class="col">
Last Digital Passport (Last Dpp):
</div>
<div class="col">
- not detected -
</div>
</div>
{% endif %}
{% if before_dpp %}
<div class="row">
<div class="col">
Before Digital Passport (Before Dpp):
</div>
</div>
<div class="row">
<div class="col">
<a href="{{ url_for('did.did', id_dpp=before_dpp.key) }}"><small class="text-muted">{{ before_dpp.key }}</small></a>
</div>
</div>
{% else %}
<div class="row">
<div class="col">
Before Digital Passport (Before Dpp):
</div>
<div class="col">
- not detected -
</div>
</div>
{% endif %}
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h5 class="card-title">Components</h5>
<div class="row">
{% if components %}
<div class="list-group col">
{% for component in components|sort(attribute='type') %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ component.type }}</h5>
<small class="text-muted">{{ component.created.strftime('%H:%M %d-%m-%Y') }}</small>
</div>
<p class="mb-1">
Manufacturer: {{ component.manufacturer or '- not detected -' }}<br />
Model: {{ component.model or '- not detected -' }}<br />
{% if rol %}
Serial: {{ component.serial_number and component.serial_number.upper() or '- not detected -' }}<br />
{% endif %}
{% if component.type in ['HardDrive', 'SolidStateDrive'] %}
Chid:
<small class="text-muted">
{{ component.chid }}
</small>
{% endif %}
</p>
<small class="text-muted">
{% if component.type in ['RamModule', 'HardDrive', 'SolidStateDrive'] %}
{{ component.size }}MB
{% endif %}
</small>
</div>
{% endfor %}
</div>
{% else %}
<div class="list-group col">
<div class="list-group-item">
- not detected -
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- ======= Footer ======= -->
<div class="container">
<div class="row">
<div class="col">
<footer class="footer">
<div class="copyright">
&copy; Copyright <strong><span>Usody</span></strong>. All Rights Reserved
</div>
<div class="credits">
<a href="https://help.usody.com/en/" target="_blank">Help</a> |
<a href="https://www.usody.com/legal/privacy-policy" target="_blank">Privacy</a> |
<a href="https://www.usody.com/legal/terms" target="_blank">Terms</a>
</div>
<div class="credits">
DeviceHub
</div>
</footer><!-- End Footer -->
</div>
</div>
</div>
{% if user.is_anonymous and not rols %}
<div class="modal fade" id="validateModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Validate as <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<a class="btn btn-primary" type="button"
href="{{ url_for('core.login') }}?next={{ path }}">
User of system
</a>
{% if oidc %}
<br />
<a class="btn btn-primary mt-3" type="button" href="{{ url_for('oidc.login_other_inventory') }}?next={{ path }}">
User of other inventory
</a>
{% endif %}
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
{% else %}
<div class="modal fade" id="rolsModal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ path }}" method="get">
<div class="modal-header">
<h5 class="modal-title">Select your Role <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<select name="rol">
{% for k, v in rols %}
<option value="{{ k }}" {% if v==rol %}selected=selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Send" />
</div>
</form>
</div>
</div>
</div>
{% endif %}
</body>
<!-- Custom Code -->
{% if not user.is_anonymous and not rol %}
<script>
$(document).ready(() => {
$("#buttonRol").click();
});
</script>
{% endif %}
</html>

View File

@ -1,6 +1,8 @@
import json import json
import logging
import flask import flask
import requests
from ereuseapi.methods import API from ereuseapi.methods import API
from flask import Blueprint from flask import Blueprint
from flask import current_app as app from flask import current_app as app
@ -9,15 +11,18 @@ from flask.json import jsonify
from flask.views import View from flask.views import View
from ereuse_devicehub import __version__ from ereuse_devicehub import __version__
from ereuse_devicehub.modules.dpp.models import Dpp, ALGORITHM
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.did.models import Dpp
did = Blueprint('did', __name__, url_prefix='/did') logger = logging.getLogger(__name__)
did = Blueprint('did', __name__, url_prefix='/did', template_folder='templates')
class DidView(View): class DidView(View):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
template_name = 'did/layout.html' template_name = 'anonymous.html'
def dispatch_request(self, id_dpp): def dispatch_request(self, id_dpp):
self.dpp = None self.dpp = None
@ -43,8 +48,26 @@ class DidView(View):
if self.accept_json(): if self.accept_json():
return jsonify(self.get_result()) return jsonify(self.get_result())
self.get_manuals()
self.get_template()
return render_template(self.template_name, **self.context) return render_template(self.template_name, **self.context)
def get_template(self):
rol = self.context.get('rol')
if not rol:
return
tlmp = {
"isOperator": "operator.html",
"isVerifier": "verifier.html",
"operator": "operator.html",
"Operator": "operator.html",
"verifier": "verifier.html",
"Verifier": "verifier.html",
}
self.template_name = tlmp.get(rol, self.template_name)
def accept_json(self): def accept_json(self):
if 'json' in request.headers.get('Accept', []): if 'json' in request.headers.get('Accept', []):
return True return True
@ -66,7 +89,7 @@ class DidView(View):
if not g.user.is_authenticated and not rols: if not g.user.is_authenticated and not rols:
return [] return []
if rols: if rols and rols != [('', '')]:
self.context['rols'] = rols self.context['rols'] = rols
if 'dpp' not in app.blueprints.keys(): if 'dpp' not in app.blueprints.keys():
@ -75,22 +98,13 @@ class DidView(View):
if not session.get('token_dlt'): if not session.get('token_dlt'):
return [] return []
token_dlt = session.get('token_dlt') _role = g.user.get_rols_dlt()
api_dlt = app.config.get('API_DLT') role = session.get('iota_abac_attributes', {}).get('role', '')
if not token_dlt or not api_dlt:
if not _role:
return [] return []
self.context['rols'] = _role
api = API(api_dlt, token_dlt, "ethereum") return _role
result = api.check_user_roles()
if result.get('Status') != 200:
return []
if 'Success' not in result.get('Data', {}).get('status'):
return []
rols = result.get('Data', {}).get('data', {})
self.context['rols'] = [(k, k) for k, v in rols.items() if v]
def get_rol(self): def get_rol(self):
rols = self.context.get('rols', []) rols = self.context.get('rols', [])
@ -148,14 +162,21 @@ class DidView(View):
return before_dpp return before_dpp
def get_result(self): def get_result(self):
components = []
data = { data = {
'hardware': {}, 'document': {},
'dpp': self.id_dpp, 'dpp': self.id_dpp,
'algorithm': ALGORITHM,
'components': components,
'manufacturer DPP': '',
}
result = {
'@context': ['https://ereuse.org/dpp0.json'],
'data': data,
} }
result = {'data': data}
if self.dpp: if self.dpp:
data['hardware'] = json.loads(self.dpp.snapshot.json_hw) data['document'] = self.dpp.snapshot.json_hw
last_dpp = self.get_last_dpp() last_dpp = self.get_last_dpp()
url_last = '' url_last = ''
if last_dpp: if last_dpp:
@ -163,13 +184,106 @@ class DidView(View):
did=last_dpp.key, host=app.config.get('HOST') did=last_dpp.key, host=app.config.get('HOST')
) )
data['url_last'] = url_last data['url_last'] = url_last
for c in self.dpp.snapshot.components:
components.append({c.type: c.chid})
return result return result
dpps = [] dpps = []
for d in self.device.dpps: for d in self.device.dpps:
rr = {'dpp': d.key, 'hardware': json.loads(d.snapshot.json_hw)} rr = {
'dpp': d.key,
'document': d.snapshot.json_hw,
'algorithm': ALGORITHM,
'manufacturer DPP': '',
}
dpps.append(rr) dpps.append(rr)
return {'data': dpps} return {
'@context': ['https://ereuse.org/dpp0.json'],
'data': dpps,
}
def get_manuals(self):
manuals = {
'ifixit': [],
'icecat': [],
'details': {},
'laer': [],
'energystar': {},
}
try:
params = {
"manufacturer": self.device.manufacturer,
"model": self.device.model,
}
self.params = json.dumps(params)
manuals['ifixit'] = self.request_manuals('ifixit')
manuals['icecat'] = self.request_manuals('icecat')
manuals['laer'] = self.request_manuals('laer')
manuals['energystar'] = self.request_manuals('energystar') or {}
if manuals['icecat']:
manuals['details'] = manuals['icecat'][0]
except Exception as err:
logger.error("Error: {}".format(err))
self.context['manuals'] = manuals
self.parse_energystar()
def parse_energystar(self):
if not self.context.get('manuals', {}).get('energystar'):
return
# Defined in:
# https://dev.socrata.com/foundry/data.energystar.gov/j7nq-iepp
energy_types = [
'functional_adder_allowances_kwh',
'tec_allowance_kwh',
'long_idle_watts',
'short_idle_watts',
'off_mode_watts',
'sleep_mode_watts',
'tec_of_model_kwh',
'tec_requirement_kwh',
'work_off_mode_watts',
'work_weighted_power_of_model_watts',
]
energy = {}
for field in energy_types:
energy[field] = []
for e in self.context['manuals']['energystar']:
for field in energy_types:
for k, v in e.items():
if not v:
continue
if field in k:
energy[field].append(v)
for k, v in energy.items():
if not v:
energy[k] = 0
continue
tt = sum([float(i) for i in v])
energy[k] = round(tt / len(v), 2)
self.context['manuals']['energystar'] = energy
def request_manuals(self, prefix):
url = app.config['URL_MANUALS']
if not url:
return {}
res = requests.post(url + "/" + prefix, self.params)
if res.status_code > 299:
return {}
try:
response = res.json()
except Exception:
response = {}
return response
did.add_url_rule('/<string:id_dpp>', view_func=DidView.as_view('did')) did.add_url_rule('/<string:id_dpp>', view_func=DidView.as_view('did'))

View File

View File

@ -0,0 +1,74 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,62 @@
import json
import requests
import click
from ereuseapi.methods import API
from flask import g, current_app as app
from ereuseapi.methods import register_user
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.agent.models import Person
from ereuse_devicehub.modules.dpp.utils import encrypt
class RegisterUserDlt:
# "operator", "verifier" or "witness"
def __init__(self, app) -> None:
super().__init__()
self.app = app
help = "Insert users than are in Dlt with params: path of data set file"
self.app.cli.command('dlt_register_user', short_help=help)(self.run)
@click.argument('dataset_file')
def run(self, dataset_file):
with open(dataset_file) as f:
dataset = json.loads(f.read())
for d in dataset:
self.add_user(d)
db.session.commit()
def add_user(self, data):
email = data.get("email")
name = email.split('@')[0]
password = data.get("password")
ethereum = {"data": data.get("data")}
user = User.query.filter_by(email=email).first()
if not user:
user = User(email=email, password=password)
user.individuals.add(Person(name=name))
data_eth = json.dumps(ethereum)
user.api_keys_dlt = encrypt(password, data_eth)
roles = []
token_dlt = ethereum["data"]["api_token"]
api_dlt = app.config.get('API_DLT')
api = API(api_dlt, token_dlt, "ethereum")
result = api.check_user_roles()
if result.get('Status') == 200:
if 'Success' in result.get('Data', {}).get('status'):
rols = result.get('Data', {}).get('data', {})
roles = [(k, k) for k, v in rols.items() if v]
user.rols_dlt = json.dumps(roles)
db.session.add(user)

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -0,0 +1,89 @@
from __future__ import with_statement
from logging.config import fileConfig
from alembic import context
from sqlalchemy import create_engine
from ereuse_devicehub.config import DevicehubConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None
from ereuse_devicehub.resources.models import Thing
target_metadata = Thing.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_url():
# url = os.environ["DATABASE_URL"]
url = DevicehubConfig.SQLALCHEMY_DATABASE_URI
return url
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = get_url()
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# connectable = engine_from_config(
# config.get_section(config.config_ini_section),
# prefix="sqlalchemy.",
# poolclass=pool.NullPool,
# )
url = get_url()
connectable = create_engine(url)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
version_table='alembic_module_dpp_version',
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,33 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
import citext
import teal
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -1,7 +1,7 @@
"""add api_keys_dlt to user """add api_keys_dlt to user
Revision ID: 4b7f77f121bf Revision ID: 4b7f77f121bf
Revises: af038a8a388c Revises:
Create Date: 2022-12-01 10:35:36.795035 Create Date: 2022-12-01 10:35:36.795035
""" """
@ -11,7 +11,7 @@ from alembic import context, op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '4b7f77f121bf' revision = '4b7f77f121bf'
down_revision = 'af038a8a388c' down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None

View File

@ -1,7 +1,7 @@
"""add digital passport dpp """add digital passport dpp
Revision ID: 8334535d56fa Revision ID: 8334535d56fa
Revises: 93daff872771 Revises: 4b7f77f121bf
Create Date: 2023-01-19 12:01:54.102326 Create Date: 2023-01-19 12:01:54.102326
""" """
@ -12,7 +12,7 @@ from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '8334535d56fa' revision = '8334535d56fa'
down_revision = '93daff872771' down_revision = '4b7f77f121bf'
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -45,13 +45,14 @@ def upgrade():
sa.Column('type', sa.Unicode(), nullable=False), sa.Column('type', sa.Unicode(), nullable=False),
sa.Column('documentId', citext.CIText(), nullable=True), sa.Column('documentId', citext.CIText(), nullable=True),
sa.Column('documentSignature', citext.CIText(), nullable=True), sa.Column('documentSignature', citext.CIText(), nullable=True),
sa.Column('normalizeDoc', citext.CIText(), nullable=True),
sa.Column('timestamp', sa.BigInteger(), nullable=False), sa.Column('timestamp', sa.BigInteger(), nullable=False),
sa.Column('device_id', sa.BigInteger(), nullable=False), sa.Column('device_id', sa.BigInteger(), nullable=False),
sa.Column('snapshot_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('issuer_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('issuer_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['snapshot_id'], ['action_id'],
[f'{get_inv()}.snapshot.id'], [f'{get_inv()}.action.id'],
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['device_id'], ['device_id'],
@ -121,11 +122,15 @@ def upgrade():
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}', schema=f'{get_inv()}',
) )
op.execute(f"CREATE SEQUENCE {get_inv()}.proof_seq START 1;")
op.execute(f"CREATE SEQUENCE {get_inv()}.dpp_seq START 1;")
def downgrade(): def downgrade():
op.drop_table('dpp', schema=f'{get_inv()}') op.drop_table('dpp', schema=f'{get_inv()}')
op.drop_table('proof', schema=f'{get_inv()}') op.drop_table('proof', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.proof_seq;")
op.execute(f"DROP SEQUENCE {get_inv()}.dpp_seq;")
# op.drop_index(op.f('ix_proof_created'), table_name='proof', schema=f'{get_inv()}') # op.drop_index(op.f('ix_proof_created'), table_name='proof', schema=f'{get_inv()}')
# op.drop_index(op.f('ix_proof_timestamp'), table_name='proof', schema=f'{get_inv()}') # op.drop_index(op.f('ix_proof_timestamp'), table_name='proof', schema=f'{get_inv()}')
op.drop_column('snapshot', 'phid_dpp', schema=f'{get_inv()}') op.drop_column('snapshot', 'phid_dpp', schema=f'{get_inv()}')

View File

@ -6,30 +6,41 @@ from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import backref, relationship from sqlalchemy.orm import backref, relationship
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from ereuse_devicehub.resources.action.models import ActionStatus, Snapshot from ereuse_devicehub.resources.action.models import Action, Snapshot
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.teal.db import CASCADE_OWN from ereuse_devicehub.teal.db import CASCADE_OWN
ALGORITHM = "sha3-256"
PROOF_ENUM = { PROOF_ENUM = {
'Register': 'Register', 'Register': 'Register',
'IssueDPP': 'IssueDPP', 'IssueDPP': 'IssueDPP',
'proof_of_recycling': 'proof_of_recycling', 'proof_of_recycling': 'proof_of_recycling',
'Erase': 'Erase', 'Erase': 'Erase',
'EWaste': 'EWaste',
'Recycled': 'Device_recycled',
} }
_sorted_proofs = {'order_by': lambda: Proof.created, 'collection_class': SortedSet}
_sorted_proofs = {
'order_by': lambda: Proof.created,
'collection_class': SortedSet
}
class Proof(Thing): class Proof(Thing):
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('proof_seq'), primary_key=True)
id.comment = """The identifier of the device for this database. Used only id.comment = """The identifier of the device for this database. Used only
internally for software; users should not use this.""" internally for software; users should not use this."""
documentId = Column(CIText(), nullable=True) documentId = Column(CIText(), nullable=True)
documentId.comment = "is the hash of snapshot.json_wb" documentId.comment = "is the uuid of snapshot"
documentSignature = Column(CIText(), nullable=True) documentSignature = Column(CIText(), nullable=True)
documentSignature.comment = "is the snapshot.json_wb with the signature of the user" documentSignature.comment = "is the snapshot.json_hw with the signature of the user"
normalizeDoc = Column(CIText(), nullable=True)
timestamp = Column(BigInteger, nullable=False) timestamp = Column(BigInteger, nullable=False)
type = Column(Unicode(STR_SM_SIZE), nullable=False) type = Column(Unicode(STR_SM_SIZE), nullable=False)
@ -52,21 +63,12 @@ class Proof(Thing):
primaryjoin=Device.id == device_id, primaryjoin=Device.id == device_id,
) )
snapshot_id = Column(UUID(as_uuid=True), ForeignKey(Snapshot.id), nullable=True) action_id = Column(UUID(as_uuid=True), ForeignKey(Action.id), nullable=True)
snapshot = relationship( action = relationship(
Snapshot, Action,
backref=backref('proofs', lazy=True), backref=backref('proofs', lazy=True),
collection_class=OrderedSet, collection_class=OrderedSet,
primaryjoin=Snapshot.id == snapshot_id, primaryjoin=Action.id == action_id,
)
action_status_id = Column(
UUID(as_uuid=True), ForeignKey(ActionStatus.id), nullable=True
)
action_status = relationship(
ActionStatus,
backref=backref('proofs', lazy=True),
primaryjoin=ActionStatus.id == action_status_id,
) )
@ -78,13 +80,13 @@ class Dpp(Thing):
""" """
id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id = Column(BigInteger, Sequence('dpp_seq'), primary_key=True)
key = Column(CIText(), nullable=False) key = Column(CIText(), nullable=False)
key.comment = "chid:phid, (chid it's in device and phid it's in the snapshot)" key.comment = "chid:phid, (chid it's in device and phid it's in the snapshot)"
documentId = Column(CIText(), nullable=True) documentId = Column(CIText(), nullable=True)
documentId.comment = "is the hash of snapshot.json_wb" documentId.comment = "is the uuid of snapshot"
documentSignature = Column(CIText(), nullable=True) documentSignature = Column(CIText(), nullable=True)
documentSignature.comment = "is the snapshot.json_wb with the signature of the user" documentSignature.comment = "is the snapshot.json_hw with the signature of the user"
timestamp = Column(BigInteger, nullable=False) timestamp = Column(BigInteger, nullable=False)
issuer_id = Column( issuer_id = Column(

View File

@ -0,0 +1,20 @@
import json
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
def register_user(email, password, rols="Operator"):
# rols = 'Issuer, Operator, Witness, Verifier'
user = User.query.filter_by(email=email).one()
# token_dlt = user.set_new_dlt_keys(password)
# result = user.allow_permitions(api_token=token_dlt, rols=rols)
# rols = user.get_rols(token_dlt=token_dlt)
# rols = [k for k, v in rols]
# user.rols_dlt = json.dumps(rols)
# db.session.commit()
# return result, rols
return

View File

@ -0,0 +1,63 @@
import json
import sys
from decouple import config
from ereuseapi.methods import API, register_user
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.modules.dpp.utils import encrypt
from ereuse_devicehub.resources.user.models import User
def main():
email = sys.argv[1]
password = sys.argv[2]
schema = config('DB_SCHEMA')
app = Devicehub(inventory=schema)
app.app_context().push()
api_dlt = app.config.get('API_DLT')
keyUser1 = app.config.get('API_DLT_TOKEN')
user = User.query.filter_by(email=email).one()
data = register_user(api_dlt)
api_token = data.get('data', {}).get('api_token')
data = json.dumps(data)
user.api_keys_dlt = encrypt(password, data)
result = allow_permitions(keyUser1, api_dlt, api_token)
rols = get_rols(api_dlt, api_token)
user.rols_dlt = json.dumps(rols)
db.session.commit()
return result, rols
def get_rols(api_dlt, token_dlt):
api = API(api_dlt, token_dlt, "ethereum")
result = api.check_user_roles()
if result.get('Status') != 200:
return []
if 'Success' not in result.get('Data', {}).get('status'):
return []
rols = result.get('Data', {}).get('data', {})
return [k for k, v in rols.items() if v]
def allow_permitions(keyUser1, api_dlt, token_dlt):
apiUser1 = API(api_dlt, keyUser1, "ethereum")
rols = "isOperator"
if len(sys.argv) > 3:
rols = sys.argv[3]
result = apiUser1.issue_credential(rols, token_dlt)
return result
if __name__ == '__main__':
# ['isIssuer', 'isOperator', 'isWitness', 'isVerifier']
main()

View File

@ -0,0 +1,9 @@
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
def set_dlt_user(email, password):
u = User.query.filter_by(email=email).one()
api_token = u.set_new_dlt_keys(password)
# u.allow_permitions(api_token)
db.session.commit()

View File

@ -0,0 +1,17 @@
import base64
from cryptography.fernet import Fernet
def encrypt(key, msg):
key = (key * 32)[:32]
key = base64.urlsafe_b64encode(key.encode())
f = Fernet(key)
return f.encrypt(msg.encode()).decode()
def decrypt(key, msg):
key = (key * 32)[:32]
key = base64.urlsafe_b64encode(key.encode())
f = Fernet(key)
return f.decrypt(msg.encode()).decode()

View File

@ -0,0 +1,38 @@
import json
from flask import Blueprint
from flask.views import View
from .models import Proof, Dpp, ALGORITHM
dpp = Blueprint('dpp', __name__, url_prefix='/', template_folder='templates')
class ProofView(View):
methods = ['GET']
def dispatch_request(selfi, proof_id):
proof = Proof.query.filter_by(timestamp=proof_id).first()
if not proof:
proof = Dpp.query.filter_by(timestamp=proof_id).one()
document = proof.snapshot.json_hw
else:
document = proof.normalizeDoc
data = {
"algorithm": ALGORITHM,
"document": document
}
d = {
'@context': ['https://ereuse.org/proof0.json'],
'data': data,
}
return json.dumps(d)
##########
# Routes #
##########
dpp.add_url_rule('/proofs/<int:proof_id>', view_func=ProofView.as_view('proof'))

View File

@ -0,0 +1,74 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,112 @@
import json
import click
import logging
import time
from werkzeug.security import gen_salt
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.modules.oidc.models import MemberFederated, OAuth2Client
logger = logging.getLogger(__name__)
class AddContractOidc:
def __init__(self, app) -> None:
super().__init__()
self.app = app
help = "Add client oidc"
self.app.cli.command('add_contract_oidc', short_help=help)(self.run)
@click.argument('email')
@click.argument('client_name')
@click.argument('client_uri')
@click.argument('scope', required=False, default="openid profile rols")
@click.argument('redirect_uris', required=False)
@click.argument('grant_types', required=False, default=["authorization_code"])
@click.argument('response_types', required=False, default=["code"])
@click.argument('token_endpoint_auth_method', required=False, default="client_secret_basic")
def run(
self,
email,
client_name,
client_uri,
scope,
redirect_uris,
grant_types,
response_types,
token_endpoint_auth_method):
self.email = email
self.client_name = client_name
self.client_uri = client_uri
self.scope = scope
self.redirect_uris = redirect_uris
self.grant_types = grant_types
self.response_types = response_types
self.token_endpoint_auth_method = token_endpoint_auth_method
if not self.redirect_uris:
self.redirect_uris = ["{}/allow_code".format(client_uri)]
self.member = MemberFederated.query.filter_by(domain=client_uri).first()
self.user = User.query.filter_by(email=email).one()
if not self.member:
txt = "This domain is not federated."
logger.error(txt)
return
if self.member.user and self.member.user != self.user:
txt = "This domain is register from other user."
logger.error(txt)
return
if self.member.client_id and self.member.client_secret:
result = {
"client_id": self.member.client_id,
"client_secret": self.member.client_secret
}
print(json.dumps(result))
return result
result = self.save()
result = {
"client_id": result[0],
"client_secret": result[1]
}
print(json.dumps(result))
return result
def save(self):
client_id = gen_salt(24)
client = OAuth2Client(client_id=client_id, user_id=self.user.id)
client.client_id_issued_at = int(time.time())
if self.token_endpoint_auth_method == 'none':
client.client_secret = ''
else:
client.client_secret = gen_salt(48)
self.member.client_id = client.client_id
self.member.client_secret = client.client_secret
self.member.user = self.user
client_metadata = {
"client_name": self.client_name,
"client_uri": self.client_uri,
"grant_types": self.grant_types,
"redirect_uris": self.redirect_uris,
"response_types": self.response_types,
"scope": self.scope,
"token_endpoint_auth_method": self.token_endpoint_auth_method,
}
client.set_client_metadata(client_metadata)
client.member_id = self.member.dlt_id_provider
db.session.add(client)
db.session.commit()
return client.client_id, client.client_secret

View File

@ -0,0 +1,24 @@
import click
from ereuse_devicehub.db import db
from ereuse_devicehub.modules.oidc.models import MemberFederated
class AddMember:
def __init__(self, app) -> None:
super().__init__()
self.app = app
help = "Add member to the federated net"
self.app.cli.command('dlt_add_member', short_help=help)(self.run)
@click.argument('dlt_id_provider')
@click.argument('domain')
def run(self, dlt_id_provider, domain):
member = MemberFederated.query.filter_by(domain=domain).first()
if member:
return
member = MemberFederated(domain=domain, dlt_id_provider=dlt_id_provider)
db.session.add(member)
db.session.commit()

View File

@ -0,0 +1,25 @@
import click
from ereuse_devicehub.db import db
from ereuse_devicehub.modules.oidc.models import MemberFederated
class AddClientOidc:
def __init__(self, app) -> None:
super().__init__()
self.app = app
help = "Add client oidc"
self.app.cli.command('add_client_oidc', short_help=help)(self.run)
@click.argument('domain')
@click.argument('client_id')
@click.argument('client_secret')
def run(self, domain, client_id, client_secret):
member = MemberFederated.query.filter_by(domain=domain).first()
if not member:
return
member.client_id = client_id
member.client_secret = client_secret
db.session.commit()

View File

@ -0,0 +1,31 @@
import click
import requests
from decouple import config
class InsertMember:
def __init__(self, app) -> None:
super().__init__()
self.app = app
help = 'Add a new members to api dlt.'
self.app.cli.command('dlt_insert_members', short_help=help)(self.run)
@click.argument('domain')
def run(self, domain):
api = config("API_RESOLVER", None)
if "http" not in domain:
print("Error: you need put https:// in domain")
return
if not api:
print("Error: you need a entry var API_RESOLVER in .env")
return
api = api.strip("/")
domain = domain.strip("/")
data = {"url": domain}
url = api + '/registerURL'
res = requests.post(url, json=data)
print(res.json())
return

View File

@ -0,0 +1,47 @@
import requests
from decouple import config
from ereuse_devicehub.db import db
from ereuse_devicehub.modules.oidc.models import MemberFederated
class GetMembers:
def __init__(self, app) -> None:
super().__init__()
self.app = app
self.app.cli.command(
'dlt_rsync_members', short_help='Synchronize members of dlt.'
)(self.run)
def run(self):
api = config("API_RESOLVER", None)
if not api:
print("Error: you need a entry var API_RESOLVER in .env")
return
api = api.strip("/")
url = api + '/getAll'
res = requests.get(url)
if res.status_code != 200:
return "Error, {}".format(res.text)
response = res.json()
members = response['url']
counter = members.pop('counter')
if counter <= MemberFederated.query.count():
return "All ok"
for k, v in members.items():
id = self.clean_id(k)
member = MemberFederated.query.filter_by(dlt_id_provider=id).first()
if member:
if member.domain != v:
member.domain = v
continue
member = MemberFederated(dlt_id_provider=id, domain=v)
db.session.add(member)
db.session.commit()
return res.text
def clean_id(self, id):
return int(id.split('DH')[-1])

View File

@ -0,0 +1,159 @@
import time
from flask import g, request, session
from flask_wtf import FlaskForm
from werkzeug.security import gen_salt
from wtforms import (
BooleanField,
SelectField,
StringField,
TextAreaField,
URLField,
validators,
)
from ereuse_devicehub.db import db
from ereuse_devicehub.modules.oidc.models import MemberFederated, OAuth2Client
AUTH_METHODS = [
('client_secret_basic', 'Client Secret Basic'),
('client_secret_post', 'Client Secret Post'),
('none', ''),
]
def split_by_crlf(s):
return [v for v in s.splitlines() if v]
class CreateClientForm(FlaskForm):
client_name = StringField(
'Client Name', description="", render_kw={'class': "form-control"}
)
client_uri = URLField(
'Client url', description="", render_kw={'class': "form-control"}
)
scope = StringField(
'Allowed Scope', description="", render_kw={'class': "form-control"}
)
redirect_uris = TextAreaField(
'Redirect URIs', description="", render_kw={'class': "form-control"}
)
grant_types = TextAreaField(
'Allowed Grant Types', description="", render_kw={'class': "form-control"}
)
response_types = TextAreaField(
'Allowed Response Types', description="", render_kw={'class': "form-control"}
)
token_endpoint_auth_method = SelectField(
'Token Endpoint Auth Method',
choices=AUTH_METHODS,
description="",
render_kw={'class': "form-control, form-select"},
)
def __init__(self, *args, **kwargs):
user = g.user
self.client = OAuth2Client.query.filter_by(user_id=user.id).first()
if request.method == 'GET':
if hasattr(self.client, 'client_metadata'):
kwargs.update(self.client.client_metadata)
grant_types = '\n'.join(kwargs.get('grant_types', ["authorization_code"]))
redirect_uris = '\n'.join(kwargs.get('redirect_uris', []))
response_types = '\n'.join(kwargs.get('response_types', ["code"]))
kwargs['grant_types'] = grant_types
kwargs['redirect_uris'] = redirect_uris
kwargs['response_types'] = response_types
super().__init__(*args, **kwargs)
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
domain = self.client_uri.data
self.member = MemberFederated.query.filter_by(domain=domain).first()
if not self.member:
txt = ["This domain is not federated."]
self.client_uri.errors = txt
return False
if self.member.user and self.member.user != g.user:
txt = ["This domain is register from other user."]
self.client_uri.errors = txt
return False
return True
def save(self):
if not self.client:
client_id = gen_salt(24)
self.client = OAuth2Client(client_id=client_id, user_id=g.user.id)
self.client.client_id_issued_at = int(time.time())
if self.token_endpoint_auth_method.data == 'none':
self.client.client_secret = ''
elif not self.client.client_secret:
self.client.client_secret = gen_salt(48)
self.member.client_id = self.client.client_id
self.member.client_secret = self.client.client_secret
if not self.member.user:
self.member.user = g.user
client_metadata = {
"client_name": self.client_name.data,
"client_uri": self.client_uri.data,
"grant_types": split_by_crlf(self.grant_types.data),
"redirect_uris": split_by_crlf(self.redirect_uris.data),
"response_types": split_by_crlf(self.response_types.data),
"scope": self.scope.data,
"token_endpoint_auth_method": self.token_endpoint_auth_method.data,
}
self.client.set_client_metadata(client_metadata)
self.client.member_id = self.member.dlt_id_provider
if not self.client.id:
db.session.add(self.client)
db.session.commit()
return self.client
class AuthorizeForm(FlaskForm):
consent = BooleanField(
'Consent?', [validators.Optional()], default=False, description=""
)
class ListInventoryForm(FlaskForm):
inventory = SelectField(
'Select your inventory',
choices=[],
description="",
render_kw={'class': "form-control, form-select"},
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.inventories = MemberFederated.query.filter(
MemberFederated.client_id.isnot(None),
MemberFederated.client_secret.isnot(None),
)
for i in self.inventories:
self.inventory.choices.append((i.dlt_id_provider, i.domain))
def save(self):
next = request.args.get('next', '')
iv = self.inventories.filter_by(dlt_id_provider=self.inventory.data).first()
if not iv:
return next
session['next_url'] = next
session['oidc'] = iv.dlt_id_provider
client_id = iv.client_id
dh = iv.domain + f'/oauth/authorize?client_id={client_id}'
dh += '&scope=openid+profile+rols&response_type=code&nonce=abc'
return dh

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -0,0 +1,89 @@
from __future__ import with_statement
from logging.config import fileConfig
from alembic import context
from sqlalchemy import create_engine
from ereuse_devicehub.config import DevicehubConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None
from ereuse_devicehub.resources.models import Thing
target_metadata = Thing.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_url():
# url = os.environ["DATABASE_URL"]
url = DevicehubConfig.SQLALCHEMY_DATABASE_URI
return url
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = get_url()
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# connectable = engine_from_config(
# config.get_section(config.config_ini_section),
# prefix="sqlalchemy.",
# poolclass=pool.NullPool,
# )
url = get_url()
connectable = create_engine(url)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
version_table='alembic_module_oidc_version',
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,33 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
import citext
import teal
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,78 @@
"""code2roles
Revision ID: 96092022dadb
Revises: abba37ff5c80
Create Date: 2023-12-12 18:45:45.324285
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '96092022dadb'
down_revision = 'abba37ff5c80'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'code_roles',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('code', citext.CIText(), nullable=False),
sa.Column('roles', citext.CIText(), nullable=False),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
op.execute(f"CREATE SEQUENCE {get_inv()}.code_roles_seq;")
op.create_table(
'code_roles',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('code', citext.CIText(), nullable=False),
sa.Column('roles', citext.CIText(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.execute(f"CREATE SEQUENCE code_roles_seq;")
def downgrade():
op.drop_table('code_roles', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.code_roles_seq;")
op.drop_table('code_roles')
op.execute(f"DROP SEQUENCE code_roles_seq;")

View File

@ -0,0 +1,175 @@
"""Open Connect OIDC
Revision ID: abba37ff5c80
Revises:
Create Date: 2022-09-30 10:01:19.761864
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'abba37ff5c80'
down_revision = None
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'member_federated',
sa.Column('dlt_id_provider', sa.BigInteger(), nullable=False),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('domain', citext.CIText(), nullable=False),
sa.Column('client_id', citext.CIText(), nullable=True),
sa.Column('client_secret', citext.CIText(), nullable=True),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.PrimaryKeyConstraint('dlt_id_provider'),
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
schema=f'{get_inv()}',
)
op.create_table(
'oauth2_client',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('client_id_issued_at', sa.BigInteger(), nullable=False),
sa.Column('client_secret_expires_at', sa.BigInteger(), nullable=False),
sa.Column('client_id', citext.CIText(), nullable=False),
sa.Column('client_secret', citext.CIText(), nullable=False),
sa.Column('client_metadata', citext.CIText(), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('member_id', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(
['member_id'],
[f'{get_inv()}.member_federated.dlt_id_provider'],
ondelete='CASCADE',
),
schema=f'{get_inv()}',
)
op.create_table(
'oauth2_code',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('client_id', citext.CIText(), nullable=True),
sa.Column('code', citext.CIText(), nullable=False),
sa.Column('redirect_uri', citext.CIText(), nullable=True),
sa.Column('response_type', citext.CIText(), nullable=True),
sa.Column('scope', citext.CIText(), nullable=True),
sa.Column('nonce', citext.CIText(), nullable=True),
sa.Column('code_challenge', citext.CIText(), nullable=True),
sa.Column('code_challenge_method', citext.CIText(), nullable=True),
sa.Column('auth_time', sa.BigInteger(), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('member_id', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(
['member_id'],
[f'{get_inv()}.member_federated.dlt_id_provider'],
ondelete='CASCADE',
),
sa.UniqueConstraint('code'),
schema=f'{get_inv()}',
)
op.create_table(
'oauth2_token',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('client_id', citext.CIText(), nullable=True),
sa.Column('token_type', citext.CIText(), nullable=True),
sa.Column('access_token', citext.CIText(), nullable=False),
sa.Column('refresh_token', citext.CIText(), nullable=True),
sa.Column('scope', citext.CIText(), nullable=True),
sa.Column('issued_at', sa.BigInteger(), nullable=False),
sa.Column('access_token_revoked_at', sa.BigInteger(), nullable=False),
sa.Column('refresh_token_revoked_at', sa.BigInteger(), nullable=False),
sa.Column('expires_in', sa.BigInteger(), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('member_id', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(
['member_id'],
[f'{get_inv()}.member_federated.dlt_id_provider'],
ondelete='CASCADE',
),
sa.UniqueConstraint('access_token'),
schema=f'{get_inv()}',
)
op.execute(f"CREATE SEQUENCE {get_inv()}.oauth2_client_seq;")
op.execute(f"CREATE SEQUENCE {get_inv()}.member_federated_seq;")
op.execute(f"CREATE SEQUENCE {get_inv()}.oauth2_code_seq;")
op.execute(f"CREATE SEQUENCE {get_inv()}.oauth2_token_seq;")
def downgrade():
op.drop_table('oauth2_client', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.oauth2_client_seq;")
op.drop_table('oauth2_code', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.oauth2_code_seq;")
op.drop_table('oauth2_token', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.oauth2_token_seq;")
op.drop_table('member_federated', schema=f'{get_inv()}')
op.execute(f"DROP SEQUENCE {get_inv()}.member_federated_seq;")

View File

@ -0,0 +1,90 @@
from authlib.integrations.sqla_oauth2 import (
OAuth2AuthorizationCodeMixin,
OAuth2ClientMixin,
OAuth2TokenMixin,
)
from flask import g
from werkzeug.security import gen_salt
from flask import current_app
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.user.models import User
def gen_code():
return gen_salt(24)
class MemberFederated(Thing):
__tablename__ = 'member_federated'
dlt_id_provider = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(40), unique=False)
# This client_id and client_secret is used for connected to this domain as
# a client and this domain then is the server of auth
client_id = db.Column(db.String(40), unique=False, nullable=True)
client_secret = db.Column(db.String(60), unique=False, nullable=True)
user_id = db.Column(
db.UUID(as_uuid=True), db.ForeignKey(User.id, ondelete='CASCADE'), nullable=True
)
user = db.relationship(User)
def __str__(self):
return self.domain
class OAuth2Client(Thing, OAuth2ClientMixin):
__tablename__ = 'oauth2_client'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.UUID(as_uuid=True),
db.ForeignKey(User.id, ondelete='CASCADE'),
nullable=False,
default=lambda: g.user.id,
)
user = db.relationship(User)
member_id = db.Column(
db.Integer,
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
)
member = db.relationship(MemberFederated)
class OAuth2AuthorizationCode(Thing, OAuth2AuthorizationCodeMixin):
__tablename__ = 'oauth2_code'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.UUID(as_uuid=True), db.ForeignKey(User.id, ondelete='CASCADE')
)
user = db.relationship(User)
member_id = db.Column(
db.Integer,
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
)
member = db.relationship('MemberFederated')
class OAuth2Token(Thing, OAuth2TokenMixin):
__tablename__ = 'oauth2_token'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.UUID(as_uuid=True), db.ForeignKey(User.id, ondelete='CASCADE')
)
user = db.relationship(User)
member_id = db.Column(
db.Integer,
db.ForeignKey('member_federated.dlt_id_provider', ondelete='CASCADE'),
)
member = db.relationship('MemberFederated')
class CodeRoles(Thing):
# __tablename__ = 'code_roles'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(40), default=gen_code, nullable=False)
roles = db.Column(db.String(40), unique=False, nullable=False)

View File

@ -0,0 +1,172 @@
from authlib.integrations.flask_oauth2 import (
AuthorizationServer as _AuthorizationServer,
)
from authlib.integrations.flask_oauth2 import ResourceProtector
from authlib.integrations.sqla_oauth2 import (
create_bearer_token_validator,
create_query_client_func,
create_save_token_func,
)
from authlib.oauth2.rfc6749.grants import (
AuthorizationCodeGrant as _AuthorizationCodeGrant,
)
from authlib.oidc.core import UserInfo
from authlib.oidc.core.grants import OpenIDCode as _OpenIDCode
from authlib.oidc.core.grants import OpenIDHybridGrant as _OpenIDHybridGrant
from authlib.oidc.core.grants import OpenIDImplicitGrant as _OpenIDImplicitGrant
from decouple import config
from werkzeug.security import gen_salt
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
from .models import OAuth2AuthorizationCode, OAuth2Client, OAuth2Token
DUMMY_JWT_CONFIG = {
'key': config('SECRET_KEY'),
'alg': 'HS256',
'iss': config("HOST", 'https://authlib.org'),
'exp': 3600,
}
def exists_nonce(nonce, req):
return False
exists = OAuth2AuthorizationCode.query.filter_by(
client_id=req.client_id, nonce=nonce
).first()
return bool(exists)
def generate_user_info(user, scope):
if 'rols' in scope:
rols = user.rols_dlt and user.get_rols_dlt() or []
return UserInfo(rols=rols, sub=str(user.id), name=user.email)
return UserInfo(sub=str(user.id), name=user.email)
def create_authorization_code(client, grant_user, request):
code = gen_salt(48)
nonce = request.data.get('nonce')
item = OAuth2AuthorizationCode(
code=code,
client_id=client.client_id,
redirect_uri=request.redirect_uri,
scope=request.scope,
user_id=grant_user.id,
nonce=nonce,
member_id=client.member_id,
)
db.session.add(item)
db.session.commit()
return code
class AuthorizationCodeGrant(_AuthorizationCodeGrant):
def create_authorization_code(self, client, grant_user, request):
return create_authorization_code(client, grant_user, request)
def parse_authorization_code(self, code, client):
item = OAuth2AuthorizationCode.query.filter_by(
code=code, client_id=client.client_id
).first()
if item and not item.is_expired():
return item
def delete_authorization_code(self, authorization_code):
db.session.delete(authorization_code)
db.session.commit()
def authenticate_user(self, authorization_code):
return User.query.get(authorization_code.user_id)
def save_authorization_code(self, code, request):
if not request.data.get('consent'):
return code
item = OAuth2AuthorizationCode(
code=code,
client_id=request.client.client_id,
redirect_uri=request.redirect_uri,
scope=request.scope,
user_id=request.user.id,
nonce=request.data.get('nonce'),
member_id=request.client.member_id,
)
db.session.add(item)
db.session.commit()
return code
def query_authorization_code(self, code, client):
return OAuth2AuthorizationCode.query.filter_by(
code=code, client_id=client.client_id
).first()
class OpenIDCode(_OpenIDCode):
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
def get_jwt_config(self, grant):
return DUMMY_JWT_CONFIG
def generate_user_info(self, user, scope):
return generate_user_info(user, scope)
class ImplicitGrant(_OpenIDImplicitGrant):
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
def get_jwt_config(self, grant):
return DUMMY_JWT_CONFIG
def generate_user_info(self, user, scope):
return generate_user_info(user, scope)
class HybridGrant(_OpenIDHybridGrant):
def create_authorization_code(self, client, grant_user, request):
return create_authorization_code(client, grant_user, request)
def exists_nonce(self, nonce, request):
return exists_nonce(nonce, request)
def get_jwt_config(self):
return DUMMY_JWT_CONFIG
def generate_user_info(self, user, scope):
return generate_user_info(user, scope)
class AuthorizationServer(_AuthorizationServer):
def validate_consent_request(self, request=None, end_user=None):
return self.get_consent_grant(request=request, end_user=end_user)
def save_token(self, token, request):
token['member_id'] = request.client.member_id
return super().save_token(token, request)
authorization = AuthorizationServer()
require_oauth = ResourceProtector()
def config_oauth(app):
query_client = create_query_client_func(db.session, OAuth2Client)
save_token = create_save_token_func(db.session, OAuth2Token)
authorization.init_app(app, query_client=query_client, save_token=save_token)
# support all openid grants
authorization.register_grant(
AuthorizationCodeGrant,
[
OpenIDCode(require_nonce=True),
],
)
authorization.register_grant(ImplicitGrant)
authorization.register_grant(HybridGrant)
# protect resource
bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
require_oauth.register_token_validator(bearer_cls())

View File

@ -0,0 +1,22 @@
discovery = {
"issuer": "{ host }",
"authorization_endpoint": "{ host }/oauth/authorize",
"token_endpoint": "{ host }/oauth/token",
"token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"],
"token_endpoint_auth_signing_alg_values_supported": ["RS256", "ES256"],
"userinfo_endpoint": "{ host }/oauth/userinfo",
"scopes_supported": ["openid", "profile", "rols"],
"response_types_supported": ["code", "code id_token", "id_token", "token id_token"],
"userinfo_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
"userinfo_encryption_alg_values_supported": ["RSA1_5", "A128KW"],
"userinfo_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"],
"id_token_signing_alg_values_supported": ["RS256", "ES256", "HS256"],
"id_token_encryption_alg_values_supported": ["RSA1_5", "A128KW"],
"id_token_encryption_enc_values_supported": ["A128CBC-HS256", "A128GCM"],
"request_object_signing_alg_values_supported": ["none", "RS256", "ES256"],
"display_values_supported": ["page", "popup"],
"claim_types_supported": ["normal", "distributed"],
"claims_supported": [],
"claims_parameter_supported": True,
"ui_locales_supported": ["en-US"],
}

View File

@ -0,0 +1,27 @@
import sys
from decouple import config
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.modules.oidc.models import MemberFederated
def main():
schema = config('DB_SCHEMA')
app = Devicehub(inventory=schema)
app.app_context().push()
dlt_id_provider = sys.argv[1]
domain = sys.argv[2]
member = MemberFederated.query.filter_by(domain=domain).first()
if member:
return
member = MemberFederated(domain=domain, dlt_id_provider=dlt_id_provider)
db.session.add(member)
db.session.commit()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,32 @@
import sys
from decouple import config
from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.modules.oidc.models import MemberFederated
def main():
"""
We need add client_id and client_secret for every server
than we want connect.
"""
schema = config('DB_SCHEMA')
app = Devicehub(inventory=schema)
app.app_context().push()
domain = sys.argv[1]
client_id = sys.argv[2]
client_secret = sys.argv[3]
member = MemberFederated.query.filter_by(domain=domain).first()
if not member:
return
member.client_id = client_id
member.client_secret = client_secret
db.session.commit()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,39 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
</div>
<section class="section profile">
<div class="row">
<div class="col-xl-6">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
<p>{{grant.client.client_name}} is requesting:
<strong>{{ grant.request.scope }}</strong>
</p>
</div>
<form action="" method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
{% for field in form %}
{% if field != form.csrf_token %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{{ field }}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ url_for('core.user-profile') }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
</div>
<section class="section profile">
<div class="row">
<div class="col-xl-6">
<div class="card">
{% if form.client %}
<div class="card-body">
<label class="form-label"><strong>Client_id:</strong></label>
<span class="form-control border-0">{{ form.client.client_id }}</span><br />
<label class="form-label"><strong>Client_secret:</strong></label>
<span class="form-control border-0">{{ form.client.client_secret }}</span>
</div>
{% endif %}
<div class="card-body">
<form action="" method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
{% for field in form %}
{% if field != form.csrf_token %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{{ field }}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ referrer }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,72 @@
{% extends "ereuse_devicehub/base.html" %}
{% block page_title %}{{ title }} - {{ page_title }}{% endblock %}
{% block body %}
<main id="main" class="main">
{% block messages %}
{% for level, message in get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{ level}} alert-dismissible fade show" role="alert">
{% if '_message_icon' in session %}
<i class="bi bi-{{ session['_message_icon'][level]}} me-1"></i>
{% else %}
<!-- fallback if 3rd party libraries (e.g. flask_login.login_required) -->
<i class="bi bi-info-circle me-1"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endblock %}
<section class="section profile">
<div class="row">
<div class="col-xl-9">
<div class="card">
<div class="card-body">
<div class="pagetitle">
<h1>{{ title }}</h1>
</div>
<form action="" method="post" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
{% for field in form %}
{% if field != form.csrf_token %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{{ field }}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ next }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- ======= Footer ======= -->
<div class="container">
<div class="row">
<div class="col">
<footer class="footer">
<div class="copyright">
&copy; Copyright <strong><span>Usody</span></strong>. All Rights Reserved
</div>
<div class="credits">
<a href="https://help.usody.com/en/" target="_blank">Help</a> |
<a href="https://www.usody.com/legal/privacy-policy" target="_blank">Privacy</a> |
<a href="https://www.usody.com/legal/terms" target="_blank">Terms</a>
</div>
<div class="credits">
DeviceHub
</div>
</footer><!-- End Footer -->
</div>
</div>
</div>
{% endblock body %}

View File

@ -0,0 +1,326 @@
import json
import logging
import base64
import requests
from authlib.integrations.flask_oauth2 import current_token
from authlib.oauth2 import OAuth2Error
from flask import (
Blueprint,
g,
jsonify,
redirect,
render_template,
request,
session,
url_for,
current_app as app
)
from flask_login import login_required
from flask.views import View
from ereuse_devicehub import __version__, messages
from ereuse_devicehub.db import db
from ereuse_devicehub.modules.oidc.forms import (
AuthorizeForm,
CreateClientForm,
ListInventoryForm,
)
from ereuse_devicehub.modules.oidc.models import (
MemberFederated,
OAuth2Client,
CodeRoles
)
from ereuse_devicehub.modules.oidc.oauth2 import (
authorization,
generate_user_info,
require_oauth,
)
from ereuse_devicehub.views import GenericMixin
oidc = Blueprint('oidc', __name__, url_prefix='/', template_folder='templates')
logger = logging.getLogger(__name__)
##########
# Server #
##########
class CreateClientView(GenericMixin):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'create_client.html'
title = "Edit Open Id Connect Client"
def dispatch_request(self):
form = CreateClientForm()
if form.validate_on_submit():
form.save()
next_url = url_for('core.user-profile')
return redirect(next_url)
self.get_context()
self.context.update(
{
'form': form,
'title': self.title,
}
)
return render_template(self.template_name, **self.context)
class AuthorizeView(GenericMixin):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'authorize.html'
title = "Authorize"
def dispatch_request(self):
form = AuthorizeForm()
client = OAuth2Client.query.filter_by(
client_id=request.args.get('client_id')
).first()
if not client:
messages.error('Not exist client')
return redirect(url_for('core.user-profile'))
if form.validate_on_submit():
if not form.consent.data:
return redirect(url_for('core.user-profile'))
return authorization.create_authorization_response(grant_user=g.user)
try:
grant = authorization.validate_consent_request(end_user=g.user)
except OAuth2Error as error:
messages.error(error.error)
return redirect(url_for('core.user-profile'))
self.get_context()
self.context.update(
{'form': form, 'title': self.title, 'user': g.user, 'grant': grant}
)
return render_template(self.template_name, **self.context)
class IssueTokenView(GenericMixin):
methods = ['POST']
decorators = []
def dispatch_request(self):
return authorization.create_token_response()
class OauthProfileView(GenericMixin):
methods = ['GET']
decorators = []
template_name = 'authorize.html'
title = "Authorize"
@require_oauth('profile')
def dispatch_request(self):
return jsonify(generate_user_info(current_token.user, current_token.scope))
##########
# Client #
##########
class SelectInventoryView(GenericMixin):
methods = ['GET', 'POST']
decorators = []
template_name = 'select_inventory.html'
title = "Select an Inventory"
def dispatch_request(self):
host = app.config.get('HOST', '').strip("/")
next = request.args.get('next', '#')
# url = "https://ebsi-pcp-wallet-ui.vercel.app/oid4vp?"
# url += f"client_id=https://{host}&"
# url += "presentation_definition_uri=https://iotaledger.github.io"
# url += "/ebsi-stardust-components/public/presentation-definition-ex1.json"
# url += "/ebsi-stardust-components/public//presentation-definition-ereuse.json&"
# url += f"response_uri=https://{host}/allow_code_oidc4vp"
# url += "&state=1700822573400&response_type=vp_token&response_mode=direct_post"
url = app.config.get('VERIFY_URL')
if host == "localhost":
url += f"?response_uri=http://{host}:5000/allow_code_oidc4vp"
url += '&presentation_definition=["EOperatorClaim"]'
else:
url += f"?response_uri=https://{host}/allow_code_oidc4vp"
url += '&presentation_definition=["EOperatorClaim"]'
session['next_url'] = next
return redirect(url, code=302)
class AllowCodeView(GenericMixin):
methods = ['GET', 'POST']
decorators = []
userinfo = None
token = None
discovery = {}
def dispatch_request(self):
self.code = request.args.get('code')
self.oidc = session.get('oidc')
if not self.code or not self.oidc:
return self.redirect()
self.member = MemberFederated.query.filter(
MemberFederated.dlt_id_provider == self.oidc,
MemberFederated.client_id.isnot(None),
MemberFederated.client_secret.isnot(None),
).first()
if not self.member:
return self.redirect()
self.get_token()
if 'error' in self.token:
messages.error(self.token.get('error', ''))
return self.redirect()
self.get_user_info()
return self.redirect()
def get_discovery(self):
if self.discovery:
return self.discovery
try:
url_well_known = self.member.domain + '.well-known/openid-configuration'
self.discovery = requests.get(url_well_known).json()
except Exception:
self.discovery = {'code': 404}
return self.discovery
def get_token(self):
data = {'grant_type': 'authorization_code', 'code': self.code}
url = self.member.domain + '/oauth/token'
url = self.get_discovery().get('token_endpoint', url)
auth = (self.member.client_id, self.member.client_secret)
msg = requests.post(url, data=data, auth=auth)
self.token = json.loads(msg.text)
def redirect(self):
url = session.get('next_url') or '/login'
return redirect(url)
def get_user_info(self):
if self.userinfo:
return self.userinfo
if 'access_token' not in self.token:
return
url = self.member.domain + '/oauth/userinfo'
url = self.get_discovery().get('userinfo_endpoint', url)
access_token = self.token['access_token']
token_type = self.token.get('token_type', 'Bearer')
headers = {"Authorization": f"{token_type} {access_token}"}
msg = requests.get(url, headers=headers)
self.userinfo = json.loads(msg.text)
rols = self.userinfo.get('rols', [])
session['rols'] = [(k, k) for k in rols]
return self.userinfo
class AllowCodeOidc4vpView(GenericMixin):
methods = ['POST']
decorators = []
userinfo = None
token = None
discovery = {}
def dispatch_request(self):
vcredential = self.get_credential()
if not vcredential:
return jsonify({"error": "No there are credentials"})
roles = self.get_roles(vcredential)
if not roles:
return jsonify({"error": "No there are roles"})
uri = self.get_response_uri(roles)
return jsonify({"redirect_uri": uri})
def get_credential(self):
pv = request.values.get("vp_token")
self.code = request.values.get("code")
token = json.loads(base64.b64decode(pv).decode())
return token.get("verifiableCredential")
def get_roles(self, vps):
try:
for vp in vps:
roles = vp.get('credentialSubject', {}).get('role')
if roles:
return roles
except Exception:
roles = None
return roles
def get_response_uri(selfi, roles):
code = CodeRoles(roles=roles)
db.session.add(code)
db.session.commit()
host = app.config.get('HOST', '').strip("/")
if host == "localhost":
url_init = "http://{host}:5000/allow_code_oidc4vp2?code={code}"
else:
url_init = "https://{host}/allow_code_oidc4vp2?code={code}"
url = url_init.format(
host=host,
code=code.code
)
return url
class AllowCodeOidc4vp2View(View):
methods = ['GET', 'POST']
def dispatch_request(self):
self.code = request.args.get('code')
if not self.code:
return self.redirect()
self.get_user_info()
return self.redirect()
def redirect(self):
url = session.pop('next_url', '/login')
return redirect(url)
def get_user_info(self):
code = CodeRoles.query.filter_by(code=self.code).first()
if not code:
return
session['rols'] = [(k.strip(), k.strip()) for k in code.roles.split(",")]
db.session.delete(code)
db.session.commit()
##########
# Routes #
##########
oidc.add_url_rule('/create_client', view_func=CreateClientView.as_view('create_client'))
oidc.add_url_rule('/oauth/authorize', view_func=AuthorizeView.as_view('autorize_oidc'))
oidc.add_url_rule('/allow_code', view_func=AllowCodeView.as_view('allow_code'))
oidc.add_url_rule('/allow_code_oidc4vp', view_func=AllowCodeOidc4vpView.as_view('allow_code_oidc4vp'))
oidc.add_url_rule('/allow_code_oidc4vp2', view_func=AllowCodeOidc4vp2View.as_view('allow_code_oidc4vp2'))
oidc.add_url_rule('/oauth/token', view_func=IssueTokenView.as_view('oauth_issue_token'))
oidc.add_url_rule(
'/oauth/userinfo', view_func=OauthProfileView.as_view('oauth_user_info')
)
oidc.add_url_rule(
'/oidc/client/select',
view_func=SelectInventoryView.as_view('login_other_inventory'),
)

View File

@ -216,6 +216,16 @@ class ReadyDef(ActionDef):
SCHEMA = schemas.Ready SCHEMA = schemas.Ready
class EWasteDef(ActionDef):
VIEW = None
SCHEMA = schemas.EWaste
class RecycledDef(ActionDef):
VIEW = None
SCHEMA = schemas.Recycled
class RecyclingDef(ActionDef): class RecyclingDef(ActionDef):
VIEW = None VIEW = None
SCHEMA = schemas.Recycling SCHEMA = schemas.Recycling

View File

@ -485,7 +485,7 @@ class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
def get_phid(self): def get_phid(self):
"""This method is used for get the phid of the computer when the action """This method is used for get the phid of the computer when the action
was created. Usefull for get the phid of the computer were a hdd was was created. Usefull for get the phid of the computer were a hdd was
Ereased Ereased.
""" """
if self.snapshot: if self.snapshot:
return self.snapshot.device.phid() return self.snapshot.device.phid()
@ -493,9 +493,7 @@ class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
return self.parent.phid() return self.parent.phid()
return '' return ''
def register_proof(self): def connect_api(self):
"""This method is used for register a proof of erasure en dlt"""
if 'dpp' not in app.blueprints.keys() or not self.snapshot: if 'dpp' not in app.blueprints.keys() or not self.snapshot:
return return
@ -504,32 +502,53 @@ class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
token_dlt = session.get('token_dlt') token_dlt = session.get('token_dlt')
api_dlt = app.config.get('API_DLT') api_dlt = app.config.get('API_DLT')
dh_instance = app.config.get('ID_FEDERATED', 'dh1')
if not token_dlt or not api_dlt: if not token_dlt or not api_dlt:
return return
api = API(api_dlt, token_dlt, "ethereum") return API(api_dlt, token_dlt, "ethereum")
from ereuse_devicehub.resources.did.models import PROOF_ENUM, Proof def register_proof(self):
"""This method is used for register a proof of erasure en dlt."""
from ereuse_devicehub.modules.dpp.models import PROOF_ENUM, ALGORITHM
deviceCHID = self.device.chid deviceCHID = self.device.chid
docSig = hashlib.sha3_256(self.snapshot.json_wb.encode('utf-8')).hexdigest() docHash = self.snapshot.phid_dpp
docID = "{}".format(self.snapshot.uuid or '') docHashAlgorithm = ALGORITHM
issuerID = "{dh}:{user}".format(dh=dh_instance, user=g.user.id)
proof_type = PROOF_ENUM['Erase'] proof_type = PROOF_ENUM['Erase']
result = api.generate_proof(deviceCHID, docID, docSig, issuerID, proof_type) dh_instance = app.config.get('ID_FEDERATED', 'dh1')
api = self.connect_api()
if not api:
return
result = api.generate_proof(
deviceCHID,
docHashAlgorithm,
docHash,
proof_type,
dh_instance,
)
self.register_erase_proof(result)
def register_erase_proof(self, result):
from ereuse_devicehub.modules.dpp.models import PROOF_ENUM, Proof
from ereuse_devicehub.resources.enums import StatusCode from ereuse_devicehub.resources.enums import StatusCode
if result['Status'] == StatusCode.Success.value: if result['Status'] == StatusCode.Success.value:
timestamp = ( timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
result.get('Data', {}).get('data', {}).get('timestamp', time.time()) if not timestamp:
) return
d = { d = {
"type": PROOF_ENUM['Erase'], "type": PROOF_ENUM['Erase'],
"device": self.device, "device": self.device,
"snapshot": self.snapshot, "action": self.snapshot,
"documentId": self.snapshot.id,
"timestamp": timestamp, "timestamp": timestamp,
"issuer_id": g.user.id, "issuer_id": g.user.id,
"documentSignature": self.snapshot.phid_dpp,
"normalizeDoc": self.snapshot.json_hw,
} }
proof = Proof(**d) proof = Proof(**d)
db.session.add(proof) db.session.add(proof)
@ -865,7 +884,6 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
return hdds return hdds
def get_new_device(self): def get_new_device(self):
if not self.device: if not self.device:
return '' return ''
@ -881,9 +899,10 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
if 'dpp' not in app.blueprints.keys() or not self.device.hid: if 'dpp' not in app.blueprints.keys() or not self.device.hid:
return return
from ereuse_devicehub.resources.did.models import Dpp from ereuse_devicehub.modules.dpp.models import Dpp, ALGORITHM
dpp = "{chid}:{phid}".format(chid=self.device.chid, phid=self.phid_dpp) dpp = "{chid}:{phid}".format(chid=self.device.chid, phid=self.phid_dpp)
# '54f431688524825f00d6fb786d5211e642e3d4564187f9ad23627ec9d313f17c'
if Dpp.query.filter_by(key=dpp).all(): if Dpp.query.filter_by(key=dpp).all():
return return
@ -897,16 +916,15 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
return return
api = API(api_dlt, token_dlt, "ethereum") api = API(api_dlt, token_dlt, "ethereum")
docSig = hashlib.sha3_256(self.json_wb.encode('utf-8')).hexdigest() docSig = self.phid_dpp
docID = "{}".format(self.uuid or '')
issuerID = "{dh}:{user}".format(dh=dh_instance, user=g.user.id)
result = api.issue_passport(dpp, docID, docSig, issuerID) result = api.issue_passport(dpp, ALGORITHM, docSig, dh_instance)
if result['Status'] is not StatusCode.Success.value: if result['Status'] is not StatusCode.Success.value:
return return
timestamp = result['Data'].get('data', {}).get('timestamp', time.time()) timestamp = result['Data'].get('data', {}).get('timestamp', time.time())
docID = "{}".format(self.uuid or '')
d_issue = { d_issue = {
"device_id": self.device.id, "device_id": self.device.id,
"snapshot": self, "snapshot": self,
@ -974,7 +992,7 @@ class BenchmarkDataStorage(Benchmark):
write_speed = Column(Float(decimal_return_scale=2), nullable=False) write_speed = Column(Float(decimal_return_scale=2), nullable=False)
def __str__(self) -> str: def __str__(self) -> str:
return 'Read: {0:.2f} MB/s, write: {0:.2f} MB/s'.format( return 'Read: {0:.2f} MB/s, write: {0:.2f} MB/s'.format( # noqa: F523
self.read_speed, self.write_speed self.read_speed, self.write_speed
) )
@ -1702,6 +1720,185 @@ class Ready(ActionWithMultipleDevices):
""" """
class Recycled(ActionWithMultipleDevices):
def register_proof(self, doc):
"""This method is used for register a proof of erasure en dlt."""
if 'dpp' not in app.blueprints.keys():
return
if not session.get('token_dlt'):
return
if not doc:
return
self.doc = doc
token_dlt = session.get('token_dlt')
api_dlt = app.config.get('API_DLT')
dh_instance = app.config.get('ID_FEDERATED', 'dh1')
if not token_dlt or not api_dlt:
return
api = API(api_dlt, token_dlt, "ethereum")
from ereuse_devicehub.modules.dpp.models import (
PROOF_ENUM,
Proof,
ALGORITHM
)
from ereuse_devicehub.resources.enums import StatusCode
for device in self.devices:
deviceCHID = device.chid
docHash = self.generateDocSig()
docHashAlgorithm = ALGORITHM
proof_type = PROOF_ENUM['Recycled']
result = api.generate_proof(
deviceCHID,
docHashAlgorithm,
docHash,
proof_type,
dh_instance,
)
if result['Status'] == StatusCode.Success.value:
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
if not timestamp:
return
d = {
"type": PROOF_ENUM['Recycled'],
"device": device,
"action": self,
"documentId": self.id,
"timestamp": timestamp,
"issuer_id": g.user.id,
"documentSignature": docHash,
"normalizeDoc": self.doc,
}
proof = Proof(**d)
db.session.add(proof)
def generateDocSig(self):
if not self.doc:
return
return hashlib.sha3_256(self.doc.encode('utf-8')).hexdigest()
class EWaste(ActionWithMultipleDevices):
"""The device is declared as e-waste, this device is not allow use more.
Any people can declared as e-waste one device.
"""
def register_proof(self, doc):
"""This method is used for register a proof of erasure en dlt."""
if 'dpp' not in app.blueprints.keys():
return
if not session.get('token_dlt'):
return
if not doc:
return
self.doc = doc
token_dlt = session.get('token_dlt')
api_dlt = app.config.get('API_DLT')
dh_instance = app.config.get('ID_FEDERATED', 'dh1')
if not token_dlt or not api_dlt:
return
api = API(api_dlt, token_dlt, "ethereum")
from ereuse_devicehub.modules.dpp.models import (
PROOF_ENUM,
Proof,
ALGORITHM
)
from ereuse_devicehub.resources.enums import StatusCode
for device in self.devices:
deviceCHID = device.chid
docHash = self.generateDocSig()
docHashAlgorithm = ALGORITHM
proof_type = PROOF_ENUM['EWaste']
result = api.generate_proof(
deviceCHID,
docHashAlgorithm,
docHash,
proof_type,
dh_instance,
)
if result['Status'] == StatusCode.Success.value:
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
if not timestamp:
return
d = {
"type": PROOF_ENUM['EWaste'],
"device": device,
"action": self,
"documentId": self.id,
"timestamp": timestamp,
"issuer_id": g.user.id,
"documentSignature": docHash,
"normalizeDoc": self.doc,
}
proof = Proof(**d)
db.session.add(proof)
self.create_snapshot()
def generateDocSig(self):
if not self.doc:
return
return hashlib.sha3_256(self.doc.encode('utf-8')).hexdigest()
def create_snapshot(self):
for device in self.devices:
dev = device.placeholder.binding
hw = self.create_json_hw(dev)
phid = hashlib.sha3_256(hw.encode('utf-8')).hexdigest()
version = self.last_snap.version
software = self.last_snap.software
snap = Snapshot(
author=dev.owner,
uuid=uuid4(),
device=dev,
json_hw = hw,
phid_dpp = phid,
version = version,
software = software
)
db.session.add(snap)
snap.register_passport_dlt()
self.snapshot = snap
def create_json_hw(self, device):
hw = self.get_json_hw(device)
hw['e-waste'] = True
return json.dumps(hw)
def get_json_hw(self, device):
self.last_snap = None
try:
self.last_snap = device.last_action_of(Snapshot)
except Exception:
return {}
if self.last_snap.json_hw:
return json.loads(self.last_snap.json_hw)
return {}
class ToPrepare(ActionWithMultipleDevices): class ToPrepare(ActionWithMultipleDevices):
"""The device has been selected for preparation. """The device has been selected for preparation.
@ -2044,7 +2241,8 @@ class Trade(JoinedTableMixin, ActionWithMultipleTradeDocuments):
) )
lot = relationship( lot = relationship(
'Lot', 'Lot',
backref=backref('trade', lazy=True, uselist=False, cascade=CASCADE_OWN), backref=backref('trade', lazy=False, uselist=False, cascade=CASCADE_OWN),
# backref=backref('trade', lazy=True, uselist=False, cascade=CASCADE_OWN),
primaryjoin='Trade.lot_id == Lot.id', primaryjoin='Trade.lot_id == Lot.id',
) )

View File

@ -523,6 +523,14 @@ class Ready(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Ready.__doc__ __doc__ = m.Ready.__doc__
class EWaste(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.EWaste.__doc__
class Recycled(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Recycled.__doc__
class ActionStatus(Action): class ActionStatus(Action):
rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',)) rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',))
devices = NestedOn( devices = NestedOn(

View File

@ -4,7 +4,6 @@ import json
import logging import logging
import os import os
import pathlib import pathlib
import time
import uuid import uuid
from contextlib import suppress from contextlib import suppress
from fractions import Fraction from fractions import Fraction
@ -16,7 +15,7 @@ from boltons import urlutils
from citext import CIText from citext import CIText
from ereuseapi.methods import API from ereuseapi.methods import API
from flask import current_app as app from flask import current_app as app
from flask import g, request, session from flask import g, request, session, url_for
from more_itertools import unique_everseen from more_itertools import unique_everseen
from sqlalchemy import BigInteger, Boolean, Column from sqlalchemy import BigInteger, Boolean, Column
from sqlalchemy import Enum as DBEnum from sqlalchemy import Enum as DBEnum
@ -78,8 +77,8 @@ logger = logging.getLogger(__name__)
def create_code(context): def create_code(context):
_id = Device.query.order_by(Device.id.desc()).first() or 3 _id = Device.query.order_by(Device.id.desc()).first() or 1
if not _id == 3: if not _id == 1:
_id = _id.id + 1 _id = _id.id + 1
return hashcode.encode(_id) return hashcode.encode(_id)
@ -355,6 +354,12 @@ class Device(Thing):
host_url = request.host_url.strip('/') host_url = request.host_url.strip('/')
return "{}{}".format(host_url, self.url.to_text()) return "{}{}".format(host_url, self.url.to_text())
@property
def chid_link(self) -> str:
host_url = request.host_url.strip('/')
url = url_for('did.did', id_dpp=self.chid)
return "{}{}".format(host_url, url)
@property @property
def url(self) -> urlutils.URL: def url(self) -> urlutils.URL:
"""The URL where to GET this device.""" """The URL where to GET this device."""
@ -478,7 +483,8 @@ class Device(Thing):
"""The trading state, or None if no Trade action has """The trading state, or None if no Trade action has
ever been performed to this device. This extract the posibilities for to do. ever been performed to this device. This extract the posibilities for to do.
This method is performed for show in the web. This method is performed for show in the web.
If you need to do one simple and generic response you can put simple=True for that.""" If you need to do one simple and generic response you can put simple=True for that.
"""
if not hasattr(lot, 'trade'): if not hasattr(lot, 'trade'):
return return
@ -933,7 +939,7 @@ class Device(Thing):
} }
return types.get(self.type, '') return types.get(self.type, '')
def register_dlt(self): def connect_api(self):
if 'dpp' not in app.blueprints.keys() or not self.hid: if 'dpp' not in app.blueprints.keys() or not self.hid:
return return
@ -945,22 +951,62 @@ class Device(Thing):
if not token_dlt or not api_dlt: if not token_dlt or not api_dlt:
return return
api = API(api_dlt, token_dlt, "ethereum") return API(api_dlt, token_dlt, "ethereum")
result = api.register_device(self.chid) def register_dlt(self):
from ereuse_devicehub.resources.did.models import PROOF_ENUM, Proof if not app.config.get('ID_FEDERATED'):
return
api = self.connect_api()
if not api:
return
snapshot = [x for x in self.actions if x.t == 'Snapshot']
if not snapshot:
return
snapshot = snapshot[0]
from ereuse_devicehub.modules.dpp.models import ALGORITHM
result = api.register_device(
self.chid,
ALGORITHM,
snapshot.phid_dpp,
app.config.get('ID_FEDERATED')
)
self.register_proof(result)
if app.config.get('ID_FEDERATED'):
api.add_service(
self.chid,
'DeviceHub',
app.config.get('ID_FEDERATED'),
'Inventory service',
'Inv',
)
def register_proof(self, result):
from ereuse_devicehub.modules.dpp.models import PROOF_ENUM, Proof
from ereuse_devicehub.resources.enums import StatusCode from ereuse_devicehub.resources.enums import StatusCode
if result['Status'] == StatusCode.Success.value: if result['Status'] == StatusCode.Success.value:
timestamp = ( timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
result.get('Data', {}).get('data', {}).get('timestamp', time.time())
) if not timestamp:
return
snapshot = [x for x in self.actions if x.t == 'Snapshot']
if not snapshot:
return
snapshot = snapshot[0]
d = { d = {
"type": PROOF_ENUM['Register'], "type": PROOF_ENUM['Register'],
"device": self, "device": self,
"snapshot": [x for x in self.actions if x.t == 'Snapshot'][0], "action": snapshot,
"timestamp": timestamp, "timestamp": timestamp,
"issuer_id": g.user.id, "issuer_id": g.user.id,
"documentId": snapshot.id,
"documentSignature": snapshot.phid_dpp,
"normalizeDoc": snapshot.json_hw,
} }
proof = Proof(**d) proof = Proof(**d)
db.session.add(proof) db.session.add(proof)
@ -972,15 +1018,6 @@ class Device(Thing):
if isinstance(c, DataStorage): if isinstance(c, DataStorage):
c.register_dlt() c.register_dlt()
if app.config.get('ID_FEDERATED'):
api.add_service(
self.chid,
'DeviceHub',
app.config.get('ID_FEDERATED'),
'Inventory service',
'Inv',
)
def unreliable(self): def unreliable(self):
self.user_trusts = False self.user_trusts = False
i = 0 i = 0
@ -1296,6 +1333,14 @@ class Placeholder(Thing):
return docs.union(self.binding.documents) return docs.union(self.binding.documents)
return docs return docs
@property
def proofs(self):
proofs = [p for p in self.device.proofs]
if self.binding:
proofs.extend([p for p in self.binding.proofs])
proofs.sort(key=lambda x: x.created, reverse=True)
return proofs
class Computer(Device): class Computer(Device):
"""A chassis with components inside that can be processed """A chassis with components inside that can be processed

View File

@ -50,7 +50,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h3 class="nav-link mt-5" style="color: #007bc0">{{ device_real.type }} - {{ device_real.verbose_name }}</h3> <h3 class="nav-link mt-5" style="color: #993365">{{ device_real.type }} - {{ device_real.verbose_name }}</h3>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<h5 class="card-title">Basic</h5> <h5 class="card-title">Basic</h5>

View File

@ -49,7 +49,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h3 class="nav-link mt-5" style="color: #007bc0"> <h3 class="nav-link mt-5" style="color: #993365">
{{ result.result.data.hardware.device.type }} - {{ result.result.data.hardware.device.type }} -
{{ result.result.data.hardware.device.manufacturer }} {{ result.result.data.hardware.device.manufacturer }}
{{ result.result.data.hardware.device.model }} {{ result.result.data.hardware.device.model }}

View File

@ -1,4 +1,5 @@
import json import json
import requests
from uuid import uuid4 from uuid import uuid4
from citext import CIText from citext import CIText
@ -139,26 +140,35 @@ class User(UserMixin, Thing):
self.api_keys_dlt = encrypt(password, data) self.api_keys_dlt = encrypt(password, data)
def allow_permitions(self, api_token=None, rols="Operator"): def allow_permitions(self, api_token=None, rols="Operator"):
if 'dpp' not in app.blueprints.keys(): # Is discontinued over IOTA branch
return return
if not api_token: # if 'dpp' not in app.blueprints.keys():
api_token = session.get('token_dlt', '.') # return
target_user = api_token.split(".")[0]
keyUser1 = app.config.get('API_DLT_TOKEN')
api_dlt = app.config.get('API_DLT')
if not keyUser1 or not api_dlt:
return
apiUser1 = API(api_dlt, keyUser1, "ethereum") # if not api_token:
# api_token = session.get('token_dlt', '.')
# target_user = api_token.split(".")[0]
# keyUser1 = app.config.get('API_DLT_TOKEN')
# api_dlt = app.config.get('API_DLT')
# if not keyUser1 or not api_dlt:
# return
for rol in rols.split(","): # apiUser1 = API(api_dlt, keyUser1, "ethereum")
result = apiUser1.issue_credential(rol.strip(), target_user)
return result # for rol in rols.split(","):
# result = apiUser1.issue_credential(rol.strip(), target_user)
# return result
def get_rols_dlt(self): def get_rols_dlt(self):
return json.loads(self.rols_dlt) return json.loads(self.rols_dlt)
def set_rols_dlt(self, token_dlt=None):
rols = self.get_rols(self, token_dlt=token_dlt)
if rols:
self.rols_dlt = json.dumps(rols)
return rols
def get_rols(self, token_dlt=None): def get_rols(self, token_dlt=None):
if 'dpp' not in app.blueprints.keys(): if 'dpp' not in app.blueprints.keys():
@ -173,17 +183,69 @@ class User(UserMixin, Thing):
if not api_dlt: if not api_dlt:
return [] return []
api = API(api_dlt, token_dlt, "ethereum") self.get_abac_did()
role = session.get('iota_abac_attributes', {}).get('role', '')
return [(x.strip(), x.strip()) for x in role.split(",")]
result = api.check_user_roles() def _call_abac(self, path):
if result.get('Status') != 200: abac_tk = app.config.get('ABAC_TOKEN')
return [] # abac_coockie = app.config.get('ABAC_COOKIE')
domain = app.config.get('ABAC_URL')
eth_pub_key = session.get('eth_pub_key')
if 'Success' not in result.get('Data', {}).get('status'): abac_path = path
return [] if not (abac_tk and eth_pub_key and abac_path and domain):
return ''
header = {
'Authorization': f'Bearer {abac_tk}',
# 'Cookie': abac_coockie
}
url = f'{domain}{eth_pub_key}/{abac_path}'
return requests.get(url, headers=header)
def get_abac_did(self):
try:
if session.get('iota_abac_did'):
return session.get('iota_abac_did')
r = self._call_abac('did')
if not r or not r.status_code == 200:
return ''
did = r.json().get('did', '').strip()
if not did:
return ''
session['iota_abac_did'] = did
return did
except Exception:
return ''
def get_abac_attributes(self):
try:
if session.get('iota_abac_attributes'):
return session.get('iota_abac_attributes')
r = self._call_abac('attributes')
if not r or not r.status_code == 200:
return {}
data = r.json()
if not data:
return {}
result = {}
for j in data:
k = j.get('attributeURI', '').split('/')[-1].split("#")[-1]
v = j.get('attributeValue', '').strip()
if not (k and v):
continue
result[k] = v
session['iota_abac_attributes'] = result
return result
except Exception:
return {}
rols = result.get('Data', {}).get('data', {})
return [(k, k) for k, v in rols.items() if v]
class UserInventory(db.Model): class UserInventory(db.Model):

View File

@ -24,7 +24,7 @@
padding-bottom: 5px; padding-bottom: 5px;
} }
#ApplyDeviceLots { #ApplyDeviceLots {
color: #007bc0; color: #993365;
font-weight: bold; font-weight: bold;
} }
#ApplyDeviceLots.disabled { #ApplyDeviceLots.disabled {
@ -34,7 +34,7 @@
background-color: #fff; background-color: #fff;
} }
.help { .help {
color: #007bc0; color: #993365;
} }
.doTransfer { .doTransfer {
margin-top: -8px; margin-top: -8px;

View File

@ -24,7 +24,7 @@ a {
} }
a:hover { a:hover {
color: #007bc0; color: #cc0066;
text-decoration: none; text-decoration: none;
} }
@ -56,7 +56,7 @@ h1, h2, h3, h4, h5, h6 {
font-size: 24px; font-size: 24px;
margin-bottom: 0; margin-bottom: 0;
font-weight: 600; font-weight: 600;
color: #007bc0; color: #993365;
} }
/*-------------------------------------------------------------- /*--------------------------------------------------------------
@ -178,14 +178,14 @@ h1, h2, h3, h4, h5, h6 {
} }
.btn-primary { .btn-primary {
background-color: #007bc0; background-color: #993365;
border-color: #007bc0; border-color: #993365;
color: #fff; color: #fff;
} }
.btn-primary:hover, .btn-primary:focus { .btn-primary:hover, .btn-primary:focus {
background-color: #007bc0; background-color: #cc0066;
border-color: #007bc0; border-color: #cc0066;
color: #fff; color: #fff;
} }
@ -351,12 +351,12 @@ h1, h2, h3, h4, h5, h6 {
color: #2c384e; color: #2c384e;
} }
.nav-tabs-bordered .nav-link:hover, .nav-tabs-bordered .nav-link:focus { .nav-tabs-bordered .nav-link:hover, .nav-tabs-bordered .nav-link:focus {
color: #007bc0; color: #993365;
} }
.nav-tabs-bordered .nav-link.active { .nav-tabs-bordered .nav-link.active {
background-color: #fff; background-color: #fff;
color: #007bc0; color: #993365;
border-bottom: 2px solid #007bc0; border-bottom: 2px solid #993365;
} }
/*-------------------------------------------------------------- /*--------------------------------------------------------------
@ -395,7 +395,7 @@ h1, h2, h3, h4, h5, h6 {
font-size: 32px; font-size: 32px;
padding-left: 10px; padding-left: 10px;
cursor: pointer; cursor: pointer;
color: #007bc0; color: #993365;
} }
.header .search-bar { .header .search-bar {
min-width: 360px; min-width: 360px;
@ -464,7 +464,7 @@ h1, h2, h3, h4, h5, h6 {
color: #012970; color: #012970;
} }
.header-nav .nav-profile { .header-nav .nav-profile {
color: #007bc0; color: #993365;
} }
.header-nav .nav-profile img { .header-nav .nav-profile img {
max-height: 36px; max-height: 36px;
@ -631,7 +631,7 @@ h1, h2, h3, h4, h5, h6 {
align-items: center; align-items: center;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #007bc0; color: #993365;
transition: 0.3; transition: 0.3;
background: #f6f9ff; background: #f6f9ff;
padding: 10px 15px; padding: 10px 15px;
@ -640,7 +640,7 @@ h1, h2, h3, h4, h5, h6 {
.sidebar-nav .nav-link i { .sidebar-nav .nav-link i {
font-size: 16px; font-size: 16px;
margin-right: 10px; margin-right: 10px;
color: #007bc0; color: #993365;
} }
.sidebar-nav .nav-link.collapsed { .sidebar-nav .nav-link.collapsed {
color: #6c757d; color: #6c757d;
@ -650,11 +650,11 @@ h1, h2, h3, h4, h5, h6 {
color: #899bbd; color: #899bbd;
} }
.sidebar-nav .nav-link:hover { .sidebar-nav .nav-link:hover {
color: #007bc0; color: #993365;
background: #f6f9ff; background: #f6f9ff;
} }
.sidebar-nav .nav-link:hover i { .sidebar-nav .nav-link:hover i {
color: #007bc0; color: #993365;
} }
.sidebar-nav .nav-link .bi-chevron-down { .sidebar-nav .nav-link .bi-chevron-down {
margin-right: 0; margin-right: 0;
@ -685,7 +685,7 @@ h1, h2, h3, h4, h5, h6 {
border-radius: 50%; border-radius: 50%;
} }
.sidebar-nav .nav-content a:hover, .sidebar-nav .nav-content a.active { .sidebar-nav .nav-content a:hover, .sidebar-nav .nav-content a.active {
color: #007bc0; color: #993365;
} }
.sidebar-nav .nav-content a.active i { .sidebar-nav .nav-content a.active i {
background-color: #4154f1; background-color: #4154f1;
@ -1028,7 +1028,7 @@ h1, h2, h3, h4, h5, h6 {
padding: 12px 15px; padding: 12px 15px;
} }
.contact .php-email-form button[type=submit] { .contact .php-email-form button[type=submit] {
background: #007bc0; background: #993365;
border: 0; border: 0;
padding: 10px 30px; padding: 10px 30px;
color: #fff; color: #fff;
@ -1036,15 +1036,15 @@ h1, h2, h3, h4, h5, h6 {
border-radius: 4px; border-radius: 4px;
} }
.contact .php-email-form button[type=submit]:hover { .contact .php-email-form button[type=submit]:hover {
background: #007bc0; background: #993365;
} }
button[type=submit] { button[type=submit] {
background-color: #007bc0; background-color: #993365;
border-color: #007bc0; border-color: #993365;
} }
button[type=submit]:hover { button[type=submit]:hover {
background-color: #007bc0; background-color: #993365;
border-color: #007bc0; border-color: #993365;
} }
@-webkit-keyframes animate-loading { @-webkit-keyframes animate-loading {
0% { 0% {

View File

@ -240,9 +240,10 @@ class SQLAlchemy(FlaskSQLAlchemy):
metadata=None, metadata=None,
query_class=BaseQuery, query_class=BaseQuery,
model_class=Model, model_class=Model,
engine_options=None,
): ):
super().__init__( super().__init__(
app, use_native_unicode, session_options, metadata, query_class, model_class app, use_native_unicode, session_options, metadata, query_class, model_class, engine_options
) )
def create_session(self, options): def create_session(self, options):
@ -266,9 +267,10 @@ class SchemaSQLAlchemy(SQLAlchemy):
metadata=None, metadata=None,
query_class=Query, query_class=Query,
model_class=Model, model_class=Model,
engine_options=None,
): ):
super().__init__( super().__init__(
app, use_native_unicode, session_options, metadata, query_class, model_class app, use_native_unicode, session_options, metadata, query_class, model_class, engine_options
) )
# The following listeners set psql's search_path to the correct # The following listeners set psql's search_path to the correct
# schema and create the schemas accordingly # schema and create the schemas accordingly

View File

@ -13,7 +13,7 @@
<div class="d-flex justify-content-center py-4"> <div class="d-flex justify-content-center py-4">
<a href="{{ url_for('core.login') }}" class="d-flex align-items-center w-auto"> <a href="{{ url_for('core.login') }}" class="d-flex align-items-center w-auto">
<img src="https://www.upc.edu/++theme++homeupc/assets/images/logomark.png" alt=""> <img src="{{ url_for('static', filename='img/usody_logo_transparent_noicon-y-purple-120x41.png') }}" alt="">
</a> </a>
</div><!-- End Logo --> </div><!-- End Logo -->
@ -69,6 +69,12 @@
</div> </div>
</div> </div>
<div class="credits">
<a href="https://help.usody.com/en/getting-started/login-usody/" target="_blank">Help</a> |
<a href="https://www.usody.com/legal/privacy-policy" target="_blank">Privacy</a> |
<a href="https://www.usody.com/legal/terms" target="_blank">Terms</a>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -78,6 +84,19 @@
</div> </div>
</main><!-- End #main --> </main><!-- End #main -->
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Do you want to try USOdy tools?</h5>
</div>
<div class="modal-body">
Just write an email to <a href="mali:hello@usody.com">hello@usody.com</a>
</div>
</div>
</div>
</div> <!-- End register modal -->
<script> <script>
const togglePassword = document.querySelector('#togglePassword'); const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#id_password'); const password = document.querySelector('#id_password');

View File

@ -44,6 +44,9 @@
<a href="{{ url_for('oidc.create_client') }}" class="nav-link">OpenID Connect</a> <a href="{{ url_for('oidc.create_client') }}" class="nav-link">OpenID Connect</a>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#id_abac_attrs">Identity Attributes</button>
</li>
</ul> </ul>
<div class="tab-content pt-2"> <div class="tab-content pt-2">
@ -103,6 +106,29 @@
</form><!-- End Sanitization Certificate datas Form --> </form><!-- End Sanitization Certificate datas Form -->
</div> </div>
<div class="tab-pane fade pt-3" id="id_abac_attrs">
{% if current_user.get_abac_did() %}
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">Did</label>
<div class="col-md-8 col-lg-9">
<a href="https://explorer.stable.iota-ec.net/custom/search/{{ current_user.get_abac_did() }}" target="_blank">{{ current_user.get_abac_did() }}</a>
</div>
</div>
{% endif %}
{% for k, v in current_user.get_abac_attributes().items() %}
<div class="row mb-3">
<label class="col-md-4 col-lg-3 col-form-label">{{ k }}</label>
<div class="col-md-8 col-lg-9">
{% if v[:4] == 'http' %}
<a href="{{ v }}" target="_blank">{{ v }}</a>
{% else %}
{{ v }}
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div><!-- End Bordered Tabs --> </div><!-- End Bordered Tabs -->
</div> </div>

View File

@ -390,10 +390,9 @@
</div> </div>
{% endif %} {% endif %}
{% if placeholder.binding %}
<div class="tab-pane fade profile-overview" id="proofs"> <div class="tab-pane fade profile-overview" id="proofs">
<h5 class="card-title">Proofs</h5> <h5 class="card-title">Proofs</h5>
{% for proof in placeholder.binding.proofs %} {% for proof in placeholder.proofs %}
<div class="list-group col-12"> <div class="list-group col-12">
<div class="list-group-item d-flex justify-content-between align-items-center"> <div class="list-group-item d-flex justify-content-between align-items-center">
<div class="row"> <div class="row">
@ -436,7 +435,6 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>

View File

@ -226,6 +226,18 @@
Ready Ready
</a> </a>
</li> </li>
<li>
<a href="javascript:newAction('EWaste')" class="dropdown-item">
<i class="bi bi-trash-fill"></i>
E-Waste
</a>
</li>
<li>
<a href="javascript:newAction('Recycled')" class="dropdown-item">
<i class="bi bi-recycle"></i>
Recycled
</a>
</li>
</ul> </ul>
</div> </div>

View File

@ -192,7 +192,7 @@
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script> <script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
{% for dev in devices %} {% for dev in devices %}
qr_draw("{{ dev.public_link }}", "#{{ dev.dhid }}") qr_draw("{{ dev.chid_link }}", "#{{ dev.dhid }}")
{% endfor %} {% endfor %}
</script> </script>
{% endblock main %} {% endblock main %}

View File

@ -28,6 +28,10 @@ class LoginView(View):
template_name = 'ereuse_devicehub/user_login.html' template_name = 'ereuse_devicehub/user_login.html'
def dispatch_request(self): def dispatch_request(self):
# if session.get('_user_id'):
# next_url = flask.request.args.get('next')
# return flask.redirect(next_url or flask.url_for('inventory.devicelist'))
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
# Login and validate the user. # Login and validate the user.
@ -64,7 +68,14 @@ class LoginView(View):
class LogoutView(View): class LogoutView(View):
def dispatch_request(self): def dispatch_request(self):
session_vars = ['token_dlt', 'rols', 'oidc'] session_vars = [
'token_dlt',
'eth_pub_key',
'rols',
'oidc',
'iota_abac_did',
'iota_abac_attributes',
]
[session.pop(i, '') for i in session_vars] [session.pop(i, '') for i in session_vars]
next_url = flask.request.args.get('next') next_url = flask.request.args.get('next')
logout_user() logout_user()

View File

@ -11,32 +11,12 @@ from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.inventory.views import devices from ereuse_devicehub.inventory.views import devices
from ereuse_devicehub.labels.views import labels from ereuse_devicehub.labels.views import labels
from ereuse_devicehub.mail.flask_mail import Mail
from ereuse_devicehub.views import core from ereuse_devicehub.views import core
from ereuse_devicehub.workbench.views import workbench from ereuse_devicehub.workbench.views import workbench
from ereuse_devicehub.modules.did.views import did
# from flask_wtf.csrf import CSRFProtect from ereuse_devicehub.modules.dpp.views import dpp
from ereuse_devicehub.modules.oidc.views import oidc
from ereuse_devicehub.modules.oidc.oauth2 import config_oauth
# from werkzeug.middleware.profiler import ProfilerMiddleware
SENTRY_DSN = config('SENTRY_DSN', None)
if SENTRY_DSN:
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[
FlaskIntegration(),
],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
)
app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA) app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA)
app.register_blueprint(core) app.register_blueprint(core)
@ -44,17 +24,10 @@ app.register_blueprint(devices)
app.register_blueprint(labels) app.register_blueprint(labels)
app.register_blueprint(api) app.register_blueprint(api)
app.register_blueprint(workbench) app.register_blueprint(workbench)
app.register_blueprint(did)
app.register_blueprint(dpp)
app.register_blueprint(oidc)
mail = Mail(app)
app.mail = mail
# configure & enable CSRF of Flask-WTF config_oauth(app)
# NOTE: enable by blueprint to exclude API views
# TODO(@slamora: enable by default & exclude API views when decouple of Teal is completed
# csrf = CSRFProtect(app)
# csrf.protect(core)
# csrf.protect(devices)
# app.config["SQLALCHEMY_RECORD_QUERIES"] = True
# app.config['PROFILE'] = True
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
# app.run(debug=True)

View File

@ -1,4 +1,52 @@
# Please fill in these three variables
API_DLT='http://$IP_API_DLT'
API_DLT_TOKEN=$TOKEN
API_RESOLVER='http://$IP_API_RESOLVER'
ABAC_TOKEN=$ABAC_TOKEN
ABAC_USER=$ABAC_USER
ABAC_URL=$ABAC_URL
# you might change or register ID_FEDERATED if you change DEVICEHUB_HOST
ID_FEDERATED='DH12'
# TODO this should be guessed by DEVICEHUB_HOST, and avoid hardcode of ID_FEDERATED
SERVER_ID_FEDERATED='DH8'
CLIENT_ID_FEDERATED='DH5'
# Database Variables
DB_USER='dhub' DB_USER='dhub'
DB_PASSWORD='ereuse' DB_PASSWORD='ereuse'
DB_HOST='localhost' DB_HOST='localhost'
DB_DATABASE='devicehub' DB_DATABASE='dpp'
SCHEMA='dbtest'
DB_SCHEMA='dbtest'
DEVICEHUB_HOST='http://localhost:5000'
#SERVER_ID_DEVICEHUB_HOST='http://devicehub-server-id.example.com'
SERVER_ID_DEVICEHUB_HOST='http://localhost:5000'
#CLIENT_ID_DEVICEHUB_HOST='http://devicehub-client-id.example.com'
CLIENT_ID_DEVICEHUB_HOST='http://localhost:5001'
SERVER_ID_SERVICE='server_id'
CLIENT_ID_SERVICE='client_id'
HOST='localhost'
EMAIL_DEMO='user@example.com'
SERVER_ID_EMAIL_DEMO='user5000@example.com'
CLIENT_ID_EMAIL_DEMO='user5001@example.com'
PASSWORD_DEMO='1234'
JWT_PASS='aaaa'
SECRET_KEY='aaaa'
# important to import snapshots (step 15)
# rel path starts with ./
SNAPSHOTS_PATH='./examples/snapshots'
# full path starts with /
#SNAPSHOTS_PATH='/tmp/dhub_docker/snapshots'
# be conservative to not upload wrong snapshots to the DLT
IMPORT_SNAPSHOTS='n'
# If you have a URL_MANUALS implementation, please change this url
URL_MANUALS='http://localhost:4000'
# on devicehub without dpp case, uncomment for a production deployment.
# That is, use of gunicorn instead of a debug python server
#DEPLOYMENT='PROD'

5
examples/pip_install.sh Normal file
View File

@ -0,0 +1,5 @@
#--index-url https://test.pypi.org/simple/
pip install --upgrade pip
pip install alembic==1.8.1 anytree==2.8.0 apispec==0.39.0 atomicwrites==1.4.0 blinker==1.5 boltons==23.0.0 cairocffi==1.4.0 cairosvg==2.5.2 certifi==2022.9.24 cffi==1.15.1 charset-normalizer==2.0.12 click==6.7 click-spinner==0.1.8 colorama==0.3.9 colour==0.1.5 cssselect2==0.7.0 defusedxml==0.7.1 et-xmlfile==1.1.0 flask==1.0.2 flask-cors==3.0.10 flask-login==0.5.0 flask-sqlalchemy==2.5.1 flask-weasyprint==0.4 flask-wtf==1.0.0 hashids==1.2.0 html5lib==1.1 idna==3.4 inflection==0.5.1 itsdangerous==2.0.1 jinja2==3.0.3 mako==1.2.3 markupsafe==2.1.1 marshmallow==3.0.0b11 marshmallow-enum==1.4.1 more-itertools==8.12.0 numpy==1.22.0 odfpy==1.4.1 openpyxl==3.0.10 pandas==1.3.5 passlib==1.7.1 phonenumbers==8.9.11 pillow==9.2.0 pint==0.9 psycopg2-binary==2.8.3 py-dmidecode==0.1.0 pycparser==2.21 pyjwt==2.4.0 pyphen==0.13.0 python-dateutil==2.7.3 python-decouple==3.3 python-dotenv==0.14.0 python-editor==1.0.4 python-stdnum==1.9 pytz==2022.2.1 pyyaml==5.4 requests==2.27.1 requests-mock==1.5.2 requests-toolbelt==0.9.1 six==1.16.0 sortedcontainers==2.1.0 sqlalchemy==1.3.24 sqlalchemy-citext==1.3.post0 sqlalchemy-utils==0.33.11 tinycss2==1.1.1 tqdm==4.32.2 urllib3==1.26.12 weasyprint==44 webargs==5.5.3 webencodings==0.5.1 werkzeug==2.0.3 wtforms==3.0.1 xlrd==2.0.1 cryptography==39.0.1 Authlib==1.2.1 gunicorn==21.2.0 xlsxwriter==3.1.9
pip install -i https://test.pypi.org/simple/ ereuseapitest==0.0.14
pip install -e .

View File

@ -0,0 +1 @@
{"closed": true, "components": [{"actions": [], "manufacturer": "Intel Corporation", "model": "82579LM Gigabit Network Connection", "serialNumber": "00:11:11:11:11:00", "speed": 1000.0, "type": "NetworkAdapter", "variant": "04", "wireless": false}, {"actions": [], "manufacturer": "Intel Corporation", "model": "7 Series/C216 Chipset Family High Definition Audio Controller", "serialNumber": null, "type": "SoundCard"}, {"actions": [], "format": "DIMM", "interface": "DDR3", "manufacturer": "Micron", "model": "16KTF51264AZ", "serialNumber": "AAAAAAAA", "size": 4096.0, "speed": 1600.0, "type": "RamModule"}, {"actions": [{"endTime": "2022-10-11T13:45:31.239555+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.623967+00:00", "steps": [{"endTime": "2021-10-11T11:05:28.090897+00:00", "severity": "Info", "startTime": "2021-10-11T09:45:19.624163+00:00", "type": "StepZero"}, {"endTime": "2021-10-11T13:45:31.239402+00:00", "severity": "Info", "startTime": "2021-10-11T11:05:28.091255+00:00", "type": "StepRandom"}], "type": "EraseSectors"}, {"assessment": true, "commandTimeout": 30, "currentPendingSectorCount": 0, "elapsed": 60, "length": "Short", "lifetime": 18720, "offlineUncorrectable": 0, "powerCycleCount": 2147, "reallocatedSectorCount": 0, "reportedUncorrectableErrors": 0, "severity": "Info", "status": "Completed without error", "type": "TestDataStorage"}, {"elapsed": 11, "readSpeed": 119.0, "type": "BenchmarkDataStorage", "writeSpeed": 32.7}], "interface": "ATA", "manufacturer": "Seagate", "model": "ST3500418AS", "serialNumber": "AAAAAAAA", "size": 500000.0, "type": "HardDrive", "variant": "CC46"}, {"actions": [{"elapsed": 0, "rate": 25540.36, "type": "BenchmarkProcessor"}, {"elapsed": 8, "rate": 7.6939, "type": "BenchmarkProcessorSysbench"}], "address": 64, "brand": "Core i5", "cores": 4, "generation": 3, "manufacturer": "Intel Corp.", "model": "Intel Core i5-3470 CPU @ 3.20GHz", "serialNumber": null, "speed": 1.6242180000000002, "threads": 4, "type": "Processor"}, {"actions": [], "manufacturer": "Intel Corporation", "memory": null, "model": "Xeon E3-1200 v2/3rd Gen Core processor Graphics Controller", "serialNumber": null, "type": "GraphicCard"}, {"actions": [], "biosDate": "2012-08-07T00:00:00", "firewire": 0, "manufacturer": "LENOVO", "model": "MAHOBAY", "pcmcia": 0, "ramMaxSize": 32, "ramSlots": 4, "serial": 1, "serialNumber": null, "slots": 4, "type": "Motherboard", "usb": 3, "version": "9SKT39AUS"}], "device": {"actions": [{"elapsed": 1, "rate": 0.6507, "type": "BenchmarkRamSysbench"}], "chassis": "Tower", "manufacturer": "LENOVO", "model": "3227A2G", "serialNumber": "AAAAAAAA", "sku": "LENOVO_MT_3227", "type": "Desktop", "version": "ThinkCentre M92P"}, "elapsed": 187302510, "endTime": "2016-11-03T17:17:01.116554+00:00", "software": "Workbench", "type": "Snapshot", "uuid": "ae913de1-e639-476a-ad9b-78eabbe4628b", "version": "11.0b11"}

View File

@ -0,0 +1,8 @@
[
{
"email": "user@example.org",
"password": "1234",
"eth_pub_key": "0x0000000000000000000000000000000000000000",
"eth_priv_key":"0x0000000000000000000000000000000000000000000000000000000000000000"
}
]

15
launcher.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/sh
set -e
set -u
# DEBUG
set -x
main() {
cd "$(dirname "${0}")"
make docker_build
docker compose up
}
main "${@}"

View File

@ -1,2 +1,2 @@
"DHID";"Lot Id";"Lot Name";"Lot Type";"Transfer Status";"Transfer Code";"Transfer Date";"Transfer Creation Date";"Transfer Update Date" "DHID";"Lot Id";"Lot Name";"Lot Type";"Transfer Status";"Transfer Code";"Transfer Date";"Transfer Creation Date";"Transfer Update Date"
"O48N2";"b33c5a0d-bc80-453f-805a-560fab88a761";"lot1";"Temporary";"";"";"";"";"" "93652";"b33c5a0d-bc80-453f-805a-560fab88a761";"lot1";"Temporary";"";"";"";"";""

1 DHID Lot Id Lot Name Lot Type Transfer Status Transfer Code Transfer Date Transfer Creation Date Transfer Update Date
2 O48N2 93652 b33c5a0d-bc80-453f-805a-560fab88a761 lot1 Temporary

View File

@ -160,7 +160,7 @@ def test_metrics_action_status(user: UserClient, user2: UserClient):
head += '"Status Supplier Created Date";"Status Receiver Created Date";"Trade-Weight";' head += '"Status Supplier Created Date";"Status Receiver Created Date";"Trade-Weight";'
head += '"Action-Create";"Allocate-Start";"Allocate-User-Code";"Allocate-NumUsers";' head += '"Action-Create";"Allocate-Start";"Allocate-User-Code";"Allocate-NumUsers";'
head += '"UsageTimeAllocate";"Type";"LiveCreate";"UsageTimeHdd"\n' head += '"UsageTimeAllocate";"Type";"LiveCreate";"UsageTimeHdd"\n'
body = '"O48N2";"adebcc5506213fac43cd8473a9c81bcf0cadaed9cb98b2eae651e377a3533c5a";' body = '"93652";"adebcc5506213fac43cd8473a9c81bcf0cadaed9cb98b2eae651e377a3533c5a";'
body += '"";"Status";"";"foo@foo.com";"Receiver";"";"";"Use";"";"' body += '"";"Status";"";"foo@foo.com";"Receiver";"";"";"Use";"";"'
assert head in csv_str assert head in csv_str
assert body in csv_str assert body in csv_str
@ -207,7 +207,7 @@ def test_complet_metrics_with_trade(user: UserClient, user2: UserClient):
query=[('filter', {'type': ['Computer'], 'ids': devices_id})], query=[('filter', {'type': ['Computer'], 'ids': devices_id})],
) )
body1_lenovo = '"O48N2";"adebcc5506213fac43cd8473a9c81bcf0cadaed9cb98b2eae651e377a3533c5a";"";"Trade";"foo@foo.com";' body1_lenovo = '"93652";"adebcc5506213fac43cd8473a9c81bcf0cadaed9cb98b2eae651e377a3533c5a";"";"Trade";"foo@foo.com";'
body1_lenovo += '"foo2@foo.com";"Supplier";"NeedConfirmation";"Use";"";' body1_lenovo += '"foo2@foo.com";"Supplier";"NeedConfirmation";"Use";"";'
body2_lenovo = ';"";"0";"0";"Trade";"0";"0"\n' body2_lenovo = ';"";"0";"0";"Trade";"0";"0"\n'
@ -232,7 +232,7 @@ def test_complet_metrics_with_trade(user: UserClient, user2: UserClient):
query=[('filter', {'type': ['Computer'], 'ids': devices_id})], query=[('filter', {'type': ['Computer'], 'ids': devices_id})],
) )
body1_lenovo = '"O48N2";"adebcc5506213fac43cd8473a9c81bcf0cadaed9cb98b2eae651e377a3533c5a";"";"Trade";"foo@foo.com";' body1_lenovo = '"93652";"adebcc5506213fac43cd8473a9c81bcf0cadaed9cb98b2eae651e377a3533c5a";"";"Trade";"foo@foo.com";'
body1_lenovo += '"foo2@foo.com";"Supplier";"NeedConfirmation";"Use";"Use";' body1_lenovo += '"foo2@foo.com";"Supplier";"NeedConfirmation";"Use";"Use";'
body2_lenovo = ';"";"0";"0";"Trade";"0";"0"\n' body2_lenovo = ';"";"0";"0";"Trade";"0";"0"\n'
body2_acer = ';"";"0";"0";"Trade";"0";"4692.0"\n' body2_acer = ';"";"0";"0";"Trade";"0";"4692.0"\n'

View File

@ -531,7 +531,7 @@ def test_add_monitor(user3: UserClientFlask):
assert dev.placeholder.id_device_supplier == "b2" assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab' assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert phid == '1' assert phid == '1'
assert dhid == 'O48N2' assert dhid == '93652'
txt = f'Device &#34;{typ}&#34; placeholder with PHID {phid} and DHID {dhid} ' txt = f'Device &#34;{typ}&#34; placeholder with PHID {phid} and DHID {dhid} '
txt += 'created successfully' txt += 'created successfully'
@ -571,7 +571,7 @@ def test_update_monitor(user3: UserClientFlask):
assert dev.placeholder.id_device_supplier == "b2" assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab' assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert phid == '1' assert phid == '1'
assert dhid == 'O48N2' assert dhid == '93652'
assert dev.model == 'lc27t55' assert dev.model == 'lc27t55'
assert dev.depth == 0.1 assert dev.depth == 0.1
assert dev.placeholder.pallet == "l34" assert dev.placeholder.pallet == "l34"
@ -640,7 +640,7 @@ def test_add_2_monitor(user3: UserClientFlask):
assert dev.placeholder.id_device_supplier == "b1" assert dev.placeholder.id_device_supplier == "b1"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab' assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert phid == '1' assert phid == '1'
assert dhid == 'O48N2' assert dhid == '93652'
assert dev.model == 'lc27t55' assert dev.model == 'lc27t55'
assert dev.placeholder.pallet == "l34" assert dev.placeholder.pallet == "l34"
@ -717,7 +717,7 @@ def test_add_laptop(user3: UserClientFlask):
dev.chid == '274f05421e4d394c5b3cd10266fed6f0500029b104b5db3521689bda589e3150' dev.chid == '274f05421e4d394c5b3cd10266fed6f0500029b104b5db3521689bda589e3150'
) )
assert phid == '1' assert phid == '1'
assert dhid == 'O48N2' assert dhid == '93652'
txt = f'Device &#34;{typ}&#34; placeholder with PHID {phid} and DHID {dhid} ' txt = f'Device &#34;{typ}&#34; placeholder with PHID {phid} and DHID {dhid} '
txt += 'created successfully' txt += 'created successfully'
@ -1891,7 +1891,7 @@ def test_edit_laptop(user3: UserClientFlask):
assert dev.serial_number == 'aaaab' assert dev.serial_number == 'aaaab'
assert dev.model == 'lc27t55' assert dev.model == 'lc27t55'
assert phid == '1' assert phid == '1'
assert dhid == 'O48N2' assert dhid == '93652'
txt = f'Device &#34;{typ}&#34; placeholder with PHID {phid} and DHID {dhid} ' txt = f'Device &#34;{typ}&#34; placeholder with PHID {phid} and DHID {dhid} '
txt += 'created successfully' txt += 'created successfully'