Compare commits
217 Commits
feature/44
...
idhub
Author | SHA1 | Date |
---|---|---|
pedro | c4dff2afb6 | |
pedro | a0b14461b3 | |
pedro | b607eedf5f | |
Cayo Puigdefabregas | 8b4f4b2c6e | |
Cayo Puigdefabregas | 2a7a10178c | |
Cayo Puigdefabregas | 2283f20ab2 | |
Cayo Puigdefabregas | a4c7b2a744 | |
Cayo Puigdefabregas | da8d43f9f6 | |
Cayo Puigdefabregas | f0710e88ec | |
Cayo Puigdefabregas | 55839a26ea | |
Cayo Puigdefabregas | 39f0300a28 | |
Cayo Puigdefabregas | e5dbb09025 | |
Cayo Puigdefabregas | c948b0bca5 | |
Cayo Puigdefabregas | cd440b9931 | |
Cayo Puigdefabregas | 44b1a245b6 | |
Cayo Puigdefabregas | f9ec594a0e | |
pedro | 397e4978e2 | |
Cayo Puigdefabregas | 77dd11ea23 | |
Cayo Puigdefabregas | 2c039c0a12 | |
Cayo Puigdefabregas | 4d416f426c | |
Cayo Puigdefabregas | 74fe50b6fb | |
Cayo Puigdefabregas | 9fb2b1c94a | |
Cayo Puigdefabregas | 708dad64aa | |
Cayo Puigdefabregas | b680a3574d | |
Cayo Puigdefabregas | b6e9be306b | |
Cayo Puigdefabregas | 95700e7ba4 | |
Cayo Puigdefabregas | 5c02b424c1 | |
Cayo Puigdefabregas | 51e54450d8 | |
Cayo Puigdefabregas | 9100f315f9 | |
Cayo Puigdefabregas | 8b0d1f4b7d | |
Cayo Puigdefabregas | 46bfef2585 | |
Cayo Puigdefabregas | 660fe41b62 | |
Cayo Puigdefabregas | e687cef4a3 | |
Cayo Puigdefabregas | af338cfa4c | |
Cayo Puigdefabregas | 72c8fac29e | |
Cayo Puigdefabregas | ca273285fe | |
Cayo Puigdefabregas | 0197ddb4d1 | |
Cayo Puigdefabregas | 87e3c3e917 | |
Cayo Puigdefabregas | 5aaa88971a | |
Cayo Puigdefabregas | 16ae2c08da | |
Cayo Puigdefabregas | 6a75423532 | |
Cayo Puigdefabregas | c9238e8e6a | |
Cayo Puigdefabregas | 53289b0dfc | |
pedro | 14e37ef8c4 | |
Cayo Puigdefabregas | 1a5384e302 | |
Cayo Puigdefabregas | d2b4de7c41 | |
Cayo Puigdefabregas | fcc4b34424 | |
Cayo Puigdefabregas | 69d1873da4 | |
Cayo Puigdefabregas | a33613b21d | |
Cayo Puigdefabregas | 9721344244 | |
Cayo Puigdefabregas | 2806e7ab18 | |
Cayo Puigdefabregas | 37fb688f77 | |
Cayo Puigdefabregas | 6c7395c26f | |
Cayo Puigdefabregas | 18600af272 | |
Cayo Puigdefabregas | 278377090a | |
Cayo Puigdefabregas | fc7d7b4549 | |
Cayo Puigdefabregas | 543aad813d | |
Cayo Puigdefabregas | 950cc59cae | |
Cayo Puigdefabregas | ada42f291a | |
Cayo Puigdefabregas | ab4ec523c3 | |
pedro | 20ee5ae411 | |
pedro | 57eb978dc4 | |
pedro | c6ec665865 | |
pedro | 5a0990f22a | |
Cayo Puigdefabregas | 0ad35de5d6 | |
Cayo Puigdefabregas | aa966b5b93 | |
Cayo Puigdefabregas | bc3d3abcd7 | |
pedro | 9ca92f4c56 | |
Cayo Puigdefabregas | 15d6851043 | |
pedro | bcb4c69677 | |
pedro | b594022194 | |
pedro | c226138ff2 | |
Cayo Puigdefabregas | 6c3831d103 | |
Cayo Puigdefabregas | a7f5de96a5 | |
Cayo Puigdefabregas | 594fe1483f | |
Cayo Puigdefabregas | ece944ea3f | |
Cayo Puigdefabregas | ac3d318fc9 | |
Cayo Puigdefabregas | 0181bd34ae | |
Cayo Puigdefabregas | 5fa6f46acc | |
Cayo Puigdefabregas | 4ba7bcc956 | |
cayop | fde966ec13 | |
Cayo Puigdefabregas | cb0c7f1cb6 | |
Cayo Puigdefabregas | 68c342ee18 | |
Cayo Puigdefabregas | b614fad41f | |
Cayo Puigdefabregas | 7e088eefc8 | |
Cayo Puigdefabregas | 82bf535915 | |
Cayo Puigdefabregas | 843324bd17 | |
Cayo Puigdefabregas | 740007b804 | |
Cayo Puigdefabregas | c8ab0a959e | |
pedro | dce2873158 | |
pedro | 2dc40e95fe | |
pedro | 5a965e245e | |
pedro | 6a58dcc68f | |
pedro | 2c4b0006cc | |
pedro | 7a85ebd8f8 | |
pedro | 9dec42bd05 | |
pedro | 37069ff561 | |
pedro | b423a53cfe | |
pedro | 260ac90f86 | |
pedro | f37800dcd3 | |
pedro | 907bf2dba0 | |
Cayo Puigdefabregas | 0b70f42daa | |
Cayo Puigdefabregas | 8a7a9476fe | |
Cayo Puigdefabregas | 5069c793cf | |
Cayo Puigdefabregas | 0f26bf63c6 | |
Cayo Puigdefabregas | bf3474e3db | |
Cayo Puigdefabregas | 274c99db43 | |
Cayo Puigdefabregas | 2f250402e3 | |
Cayo Puigdefabregas | 3f86242bfb | |
Cayo Puigdefabregas | 5de416796e | |
Cayo Puigdefabregas | 0fb4fa5ba6 | |
Cayo Puigdefabregas | 33fc69013f | |
Cayo Puigdefabregas | d5f8b1ec75 | |
Cayo Puigdefabregas | 5aee8f3f8f | |
Cayo Puigdefabregas | 9c2f22c77a | |
Cayo Puigdefabregas | db706503fc | |
Cayo Puigdefabregas | 11f3b7730a | |
Cayo Puigdefabregas | 7857a85e8f | |
Cayo Puigdefabregas | ac02246ddc | |
Cayo Puigdefabregas | 33efa7ab75 | |
Cayo Puigdefabregas | 74789d66d1 | |
Cayo Puigdefabregas | 1a107bb2db | |
Cayo Puigdefabregas | 0d2dd2fcb1 | |
Cayo Puigdefabregas | 7f449aa95c | |
Cayo Puigdefabregas | 4f2cfe5c47 | |
Cayo Puigdefabregas | 1b4159d58b | |
Cayo Puigdefabregas | 13d36f5650 | |
Cayo Puigdefabregas | 9ff20740dd | |
Cayo Puigdefabregas | 0b0d9edaad | |
cayop | 77f39ef78c | |
cayop | 947deb45af | |
Cayo Puigdefabregas | 7c91314d4a | |
Cayo Puigdefabregas | 56b36ab244 | |
Cayo Puigdefabregas | 79cb5279e9 | |
Cayo Puigdefabregas | fdb4d90ab4 | |
Cayo Puigdefabregas | b5ae2b0629 | |
Cayo Puigdefabregas | 748516edaf | |
Cayo Puigdefabregas | 5e3af04a8c | |
Cayo Puigdefabregas | 472d742db2 | |
Cayo Puigdefabregas | 36c61d49ff | |
Cayo Puigdefabregas | 2f9a2edb44 | |
Cayo Puigdefabregas | 8762705cb5 | |
Cayo Puigdefabregas | 0438cbb509 | |
Cayo Puigdefabregas | e451668ff9 | |
Cayo Puigdefabregas | 945c0d42f9 | |
Cayo Puigdefabregas | 94ddc76e17 | |
Cayo Puigdefabregas | 453fa52963 | |
Cayo Puigdefabregas | 73fa8a6d28 | |
Cayo Puigdefabregas | a68784c94c | |
Cayo Puigdefabregas | 311ca3ca51 | |
Cayo Puigdefabregas | 1f78c184b5 | |
Cayo Puigdefabregas | a6684999a8 | |
Cayo Puigdefabregas | 8f333e04ae | |
Cayo Puigdefabregas | 52da9c99ba | |
Cayo Puigdefabregas | 7ddcb8ead0 | |
Cayo Puigdefabregas | 523ca3e892 | |
Cayo Puigdefabregas | 17c88ef4b1 | |
Cayo Puigdefabregas | 3c23c8ce09 | |
Cayo Puigdefabregas | 47e918dc07 | |
Cayo Puigdefabregas | 80486136bd | |
Cayo Puigdefabregas | 850398f7ed | |
Cayo Puigdefabregas | f7a60647b9 | |
Cayo Puigdefabregas | 4d5761ac02 | |
Cayo Puigdefabregas | 66e162db4d | |
Cayo Puigdefabregas | 79c2ecbd81 | |
Cayo Puigdefabregas | b29086e46d | |
Cayo Puigdefabregas | 9119143a63 | |
Cayo Puigdefabregas | 9671333635 | |
Cayo Puigdefabregas | e3b8543a12 | |
Cayo Puigdefabregas | 49d19ec38e | |
Cayo Puigdefabregas | 312f8a01bf | |
Cayo Puigdefabregas | c82db8caa0 | |
Cayo Puigdefabregas | 4db71b2af4 | |
Cayo Puigdefabregas | 4a6c82ef55 | |
Cayo Puigdefabregas | df6b09d051 | |
Cayo Puigdefabregas | efacba6aab | |
Cayo Puigdefabregas | 7990f4518b | |
Cayo Puigdefabregas | 46860660e0 | |
Cayo Puigdefabregas | 838d9180ad | |
Cayo Puigdefabregas | 48be3bae64 | |
Cayo Puigdefabregas | 4e610f0903 | |
Cayo Puigdefabregas | b5a77ace2f | |
Cayo Puigdefabregas | 7b2bfd095c | |
Cayo Puigdefabregas | 7a128e6e7f | |
Cayo Puigdefabregas | 3cf87f7f95 | |
Cayo Puigdefabregas | e649d65b5d | |
Cayo Puigdefabregas | ad1e5e06d9 | |
Cayo Puigdefabregas | 7dc7ca2026 | |
Cayo Puigdefabregas | e559ea30da | |
Cayo Puigdefabregas | 174928872f | |
Cayo Puigdefabregas | d1abc8075f | |
Cayo Puigdefabregas | 8f58bcb24e | |
Cayo Puigdefabregas | 327e5f20cb | |
Cayo Puigdefabregas | 2da17d06c0 | |
Cayo Puigdefabregas | c8fb5db63c | |
Cayo Puigdefabregas | b6b5e5d29d | |
Cayo Puigdefabregas | 12b196fd8e | |
Cayo Puigdefabregas | ef8825568f | |
Cayo Puigdefabregas | 599f15d5ae | |
Cayo Puigdefabregas | 1907f2508a | |
Cayo Puigdefabregas | 8efccf4f58 | |
Cayo Puigdefabregas | 6b54521d50 | |
Cayo Puigdefabregas | 32dc4445e4 | |
Cayo Puigdefabregas | e7e595f2c2 | |
Cayo Puigdefabregas | 45a787aa39 | |
Cayo Puigdefabregas | 574ab36da4 | |
Cayo Puigdefabregas | fbe8600cc1 | |
Cayo Puigdefabregas | 9053c89f47 | |
Cayo Puigdefabregas | 8ed1e2296d | |
Cayo Puigdefabregas | 8dd926de80 | |
Cayo Puigdefabregas | faa7c1d605 | |
Cayo Puigdefabregas | 8c979d7741 | |
Cayo Puigdefabregas | a0e63b2ae9 | |
Cayo Puigdefabregas | 789eb1b526 | |
Cayo Puigdefabregas | e7cf069a33 | |
Cayo Puigdefabregas | 88b13961fe | |
Cayo Puigdefabregas | 9d8fb6b04a |
|
@ -127,8 +127,16 @@ yarn.lock
|
|||
# ESLint Report
|
||||
eslint_report.json
|
||||
|
||||
modules/
|
||||
# modules/
|
||||
tmp/
|
||||
.env*
|
||||
bin/
|
||||
env*
|
||||
examples/create-db2.sh
|
||||
package-lock.json
|
||||
snapshots/
|
||||
!examples/snapshots
|
||||
modules/
|
||||
|
||||
# emacs
|
||||
*~
|
||||
|
|
|
@ -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.
|
|
@ -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
203
README.md
|
@ -1,151 +1,122 @@
|
|||
# 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 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
|
||||
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`.
|
||||
- [PostgreSQL 11 or higher](https://www.postgresql.org/download/).
|
||||
- Weasyprint [dependencie](http://weasyprint.readthedocs.io/en/stable/install.html)
|
||||
# Docker
|
||||
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.
|
||||
|
||||
Install Devicehub with *pip*: `pip3 install -U -r requirements.txt -e .`
|
||||
|
||||
# Running
|
||||
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
|
||||
1. Download the sources:
|
||||
```
|
||||
git clone https://github.com/eReuse/devicehub-teal.git -b oidc4vp
|
||||
cd devicehub-teal
|
||||
```
|
||||
|
||||
Using the `dh` tool for set up with one or multiple inventories.
|
||||
Create the tables in the database by executing:
|
||||
|
||||
```bash
|
||||
$ export dhi=dbtest; dh inv add --common --name dbtest
|
||||
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.
|
||||
```
|
||||
cp snapshot01.json examples/snapshots/
|
||||
```
|
||||
|
||||
Finally, run the app:
|
||||
|
||||
```bash
|
||||
$ export dhi=dbtest;dh run --debugger
|
||||
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
|
||||
```
|
||||
IMPORT_SNAPSHOTS='n'
|
||||
```
|
||||
|
||||
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`
|
||||
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.
|
||||
|
||||
## 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:
|
||||
```bash
|
||||
$ export dhi=dbtest; dh inv add --name dbtest
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
# 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
|
||||
```
|
||||
FILE=my_users_devicehub.json
|
||||
DOCKER_SERVICE=devicehub-id-server
|
||||
docker compose cp /path/to/${FILE} ${DOCKER_SERVICE}:/tmp/
|
||||
docker compose exec ${DOCKER_SERVICE} flask dlt_register_user /tmp/${FILE}
|
||||
```
|
||||
|
||||
Initialize the database:
|
||||
**my_users_devicehub.json** is a custom file which is similar to the one provided in `examples/users_devicehub.json`
|
||||
|
||||
```bash
|
||||
$ export dhi=dbtest; dh inv add --common --name dbtest
|
||||
5. To shut down the services and remove the corresponding data, you can use:
|
||||
```
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
This command will create the schemas, tables in the specified database.
|
||||
Then we need to stamp the initial migration.
|
||||
|
||||
```bash
|
||||
$ alembic stamp head
|
||||
If you want to enter a shell inside a **new instance of the container**:
|
||||
```
|
||||
docker run -it --entrypoint= ${target_docker_image} bash
|
||||
```
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
If you want to enter a shell on an **already running container**:
|
||||
```
|
||||
docker exec -it ${target_docker_image} bash
|
||||
```
|
||||
|
||||
This command will create a new revision file with name `<revision_id>_a_table_change`.
|
||||
Edit the generated file with the necessary operations to perform the migration:
|
||||
|
||||
```bash
|
||||
$ alembic edit <revision_id>
|
||||
To know the valid value for ${target_docker_image} you can use:
|
||||
```
|
||||
docker ps
|
||||
```
|
||||
|
||||
Apply migrations using:
|
||||
6. These are the details for use in this implementation:
|
||||
|
||||
```bash
|
||||
$ alembic -x inventory=dbtest upgrade head
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
$ alembic -x inventory=dbtest downgrade <revision_id>
|
||||
http://localhost:5001/workbench/
|
||||
```
|
||||
|
||||
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`.
|
||||
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.
|
||||
|
|
|
@ -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`.
|
|
@ -0,0 +1 @@
|
|||
docker-compose_devicehub-dpp.yml
|
|
@ -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:
|
|
@ -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:
|
|
@ -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
|
|
@ -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
|
|
@ -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 "${@}"
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
|
@ -1,5 +1,4 @@
|
|||
import json
|
||||
import logging
|
||||
from binascii import Error as asciiError
|
||||
|
||||
from flask import Blueprint
|
||||
|
@ -13,7 +12,7 @@ from werkzeug.exceptions import Unauthorized
|
|||
from ereuse_devicehub.auth import Auth
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.parser.models import SnapshotsLog
|
||||
from ereuse_devicehub.parser.parser import ParseSnapshot
|
||||
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
|
||||
from ereuse_devicehub.parser.schemas import Snapshot_lite
|
||||
from ereuse_devicehub.resources.action.views.snapshot import (
|
||||
SnapshotMixin,
|
||||
|
@ -22,8 +21,6 @@ from ereuse_devicehub.resources.action.views.snapshot import (
|
|||
)
|
||||
from ereuse_devicehub.resources.enums import Severity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
api = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
|
@ -58,19 +55,7 @@ class InventoryView(LoginMixin, SnapshotMixin):
|
|||
if type(snapshot_json) == Response:
|
||||
return snapshot_json
|
||||
|
||||
try:
|
||||
self.snapshot_json = ParseSnapshot(snapshot_json).get_snapshot()
|
||||
raise 1 == 2
|
||||
except Exception as err:
|
||||
logger.error("Error: {} \n{}\n".format(err, self.snapshot_json))
|
||||
|
||||
self.response = jsonify(
|
||||
{
|
||||
'error': err,
|
||||
}
|
||||
)
|
||||
self.response.status_code = 500
|
||||
return self.response
|
||||
self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot()
|
||||
|
||||
snapshot = self.build()
|
||||
snapshot.device.set_hid()
|
||||
|
|
|
@ -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)
|
|
@ -1,6 +1,7 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from boltons.urlutils import URL
|
||||
from decouple import config
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.agent.models import Person
|
||||
|
@ -13,6 +14,9 @@ class InitDatas:
|
|||
super().__init__()
|
||||
self.app = app
|
||||
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(
|
||||
'initdata', short_help='Save a minimum structure of datas.'
|
||||
)(self.run)
|
||||
|
@ -29,12 +33,9 @@ class InitDatas:
|
|||
db.session.add(inv)
|
||||
db.session.commit()
|
||||
|
||||
email = 'user@dhub.com'
|
||||
password = '1234'
|
||||
name = 'user'
|
||||
if self.email:
|
||||
user = User(email=self.email, password=self.password)
|
||||
user.individuals.add(Person(name=self.name))
|
||||
db.session.add(user)
|
||||
|
||||
user = User(email=email, password=password)
|
||||
user.individuals.add(Person(name=name))
|
||||
db.session.add(user)
|
||||
|
||||
db.session.commit()
|
||||
db.session.commit()
|
||||
|
|
|
@ -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)
|
|
@ -1,4 +1,4 @@
|
|||
from distutils.version import LooseVersion
|
||||
from distutils.version import StrictVersion
|
||||
from itertools import chain
|
||||
|
||||
from decouple import config
|
||||
|
@ -13,6 +13,7 @@ from ereuse_devicehub.resources import (
|
|||
user,
|
||||
)
|
||||
from ereuse_devicehub.resources.device import definitions
|
||||
from ereuse_devicehub.resources.did import did
|
||||
from ereuse_devicehub.resources.documents import documents
|
||||
from ereuse_devicehub.resources.enums import PriceSoftware
|
||||
from ereuse_devicehub.resources.licences import licences
|
||||
|
@ -32,6 +33,7 @@ class DevicehubConfig(Config):
|
|||
import_resource(action),
|
||||
import_resource(user),
|
||||
import_resource(tag),
|
||||
import_resource(did),
|
||||
import_resource(agent),
|
||||
import_resource(lot),
|
||||
import_resource(deliverynote),
|
||||
|
@ -50,20 +52,28 @@ class DevicehubConfig(Config):
|
|||
DB_HOST = config('DB_HOST', 'localhost')
|
||||
DB_DATABASE = config('DB_DATABASE', 'devicehub')
|
||||
DB_SCHEMA = config('DB_SCHEMA', 'dbtest')
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://{user}:{pw}@{host}/{db}'.format(
|
||||
user=DB_USER,
|
||||
pw=DB_PASSWORD,
|
||||
host=DB_HOST,
|
||||
db=DB_DATABASE,
|
||||
) # 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')
|
||||
HOST = config('HOST', 'localhost')
|
||||
API_HOST = config('API_HOST', 'localhost')
|
||||
MIN_WORKBENCH = LooseVersion('11.0a1')
|
||||
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
|
||||
"""The minimum version of ereuse.org workbench that this devicehub
|
||||
accepts. we recommend not changing this value.
|
||||
"""
|
||||
SCHEMA_HWMD = ["1.0.0"]
|
||||
SCHEMA_WORKBENCH = ["1.0.0"]
|
||||
|
||||
TMP_SNAPSHOTS = config('TMP_SNAPSHOTS', '/tmp/snapshots')
|
||||
TMP_LIVES = config('TMP_LIVES', '/tmp/lives')
|
||||
|
@ -75,7 +85,7 @@ class DevicehubConfig(Config):
|
|||
API_DOC_CLASS_DISCRIMINATOR = 'type'
|
||||
|
||||
PRICE_SOFTWARE = PriceSoftware.Ereuse
|
||||
PRICE_VERSION = LooseVersion('1.0')
|
||||
PRICE_VERSION = StrictVersion('1.0')
|
||||
PRICE_CURRENCY = Currency.EUR
|
||||
"""Official versions."""
|
||||
|
||||
|
@ -93,3 +103,22 @@ class DevicehubConfig(Config):
|
|||
MAIL_PORT = config('MAIL_PORT', 587)
|
||||
MAIL_USE_TLS = config('MAIL_USE_TLS', True)
|
||||
MAIL_DEFAULT_SENDER = config('MAIL_DEFAULT_SENDER', '')
|
||||
API_DLT = config('API_DLT', None)
|
||||
API_DLT_TOKEN = config('API_DLT_TOKEN', 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."""
|
||||
OAUTH2_JWT_ENABLED = config('OAUTH2_JWT_ENABLED', False)
|
||||
OAUTH2_JWT_ISS = config('OAUTH2_JWT_ISS', '')
|
||||
OAUTH2_JWT_KEY = config('OAUTH2_JWT_KEY', None)
|
||||
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)
|
||||
|
|
|
@ -70,6 +70,8 @@ def create_view(name, selectable):
|
|||
return table
|
||||
|
||||
|
||||
db = SQLAlchemy(session_options={'autoflush': False})
|
||||
db = SQLAlchemy(
|
||||
session_options={'autoflush': False},
|
||||
)
|
||||
f = db.func
|
||||
exp = expression
|
||||
|
|
|
@ -13,7 +13,9 @@ import ereuse_devicehub.ereuse_utils.cli
|
|||
from ereuse_devicehub.auth import Auth
|
||||
from ereuse_devicehub.client import Client, UserClient
|
||||
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.snapshots import UploadSnapshots
|
||||
|
||||
# from ereuse_devicehub.commands.reports import Report
|
||||
from ereuse_devicehub.commands.users import GetToken
|
||||
|
@ -28,6 +30,36 @@ from ereuse_devicehub.teal.db import ResourceNotFound, SchemaSQLAlchemy
|
|||
from ereuse_devicehub.teal.teal import Teal
|
||||
from ereuse_devicehub.templating import Environment
|
||||
|
||||
try:
|
||||
from ereuse_devicehub.modules.oidc.commands.sync_dlt import GetMembers
|
||||
except Exception:
|
||||
GetMembers = None
|
||||
|
||||
try:
|
||||
from ereuse_devicehub.modules.dpp.commands.register_user_dlt import RegisterUserDlt
|
||||
except Exception:
|
||||
RegisterUserDlt = None
|
||||
|
||||
try:
|
||||
from ereuse_devicehub.modules.oidc.commands.add_member import AddMember
|
||||
except Exception:
|
||||
AddMember = None
|
||||
|
||||
try:
|
||||
from ereuse_devicehub.modules.oidc.commands.client_member import AddClientOidc
|
||||
except Exception:
|
||||
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):
|
||||
test_client_class = Client
|
||||
|
@ -77,6 +109,22 @@ class Devicehub(Teal):
|
|||
self.get_token = GetToken(self)
|
||||
self.initdata = InitDatas(self)
|
||||
self.adduser = AddUser(self)
|
||||
self.uploadsnapshots = UploadSnapshots(self)
|
||||
self.checkinstall = CheckInstall(self)
|
||||
|
||||
if GetMembers:
|
||||
self.get_members = GetMembers(self)
|
||||
if RegisterUserDlt:
|
||||
self.dlt_register_user = RegisterUserDlt(self)
|
||||
if AddMember:
|
||||
self.dlt_insert_members = AddMember(self)
|
||||
if AddClientOidc:
|
||||
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(
|
||||
short_help='Inventory management.',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from boltons.urlutils import URL
|
||||
from flask import g
|
||||
from flask import current_app as app
|
||||
from flask import g, session
|
||||
from flask_wtf import FlaskForm
|
||||
from werkzeug.security import generate_password_hash
|
||||
from wtforms import (
|
||||
|
@ -68,6 +69,17 @@ class LoginForm(FlaskForm):
|
|||
if not user.is_active:
|
||||
self.form_errors.append(self.error_messages['inactive'])
|
||||
|
||||
if 'dpp' in app.blueprints.keys():
|
||||
dlt_keys = user.get_dlt_keys(
|
||||
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['eth_pub_key'] = eth_pub_key
|
||||
session['rols'] = user.get_rols()
|
||||
|
||||
return user.is_active
|
||||
|
||||
|
||||
|
@ -103,6 +115,17 @@ class PasswordForm(FlaskForm):
|
|||
return True
|
||||
|
||||
def save(self, commit=True):
|
||||
if 'dpp' in app.blueprints.keys():
|
||||
keys_dlt = g.user.get_dlt_keys(self.password.data)
|
||||
g.user.reset_dlt_keys(self.newpassword.data, keys_dlt)
|
||||
|
||||
token_dlt = (
|
||||
g.user.get_dlt_keys(self.newpassword.data)
|
||||
.get('data', {})
|
||||
.get('api_token')
|
||||
)
|
||||
session['token_dlt'] = token_dlt
|
||||
|
||||
g.user.password = self.newpassword.data
|
||||
|
||||
db.session.add(g.user)
|
||||
|
@ -112,7 +135,6 @@ class PasswordForm(FlaskForm):
|
|||
|
||||
|
||||
class SanitizationEntityForm(FlaskForm):
|
||||
|
||||
logo = URLField(
|
||||
'Logo',
|
||||
[validators.Optional(), validators.URL()],
|
||||
|
|
|
@ -39,9 +39,11 @@ from ereuse_devicehub.inventory.models import (
|
|||
TransferCustomerDetails,
|
||||
)
|
||||
from ereuse_devicehub.parser.models import PlaceholdersLog, SnapshotsLog
|
||||
from ereuse_devicehub.parser.parser import ParseSnapshot # , ParseSnapshotLsHw
|
||||
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
|
||||
from ereuse_devicehub.parser.schemas import Snapshot_lite
|
||||
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.views.snapshot import (
|
||||
SnapshotMixin,
|
||||
|
@ -315,33 +317,31 @@ class UploadSnapshotForm(SnapshotMixin, FlaskForm):
|
|||
|
||||
return True
|
||||
|
||||
def save(self, commit=True, user_trusts=True): # noqa: C901
|
||||
def is_wb_lite_snapshot(self, version: str) -> bool:
|
||||
is_lite = False
|
||||
if version in app.config['SCHEMA_WORKBENCH']:
|
||||
is_lite = True
|
||||
|
||||
return is_lite
|
||||
|
||||
def save(self, commit=True, user_trusts=True):
|
||||
if any([x == 'Error' for x in self.result.values()]):
|
||||
return
|
||||
schema = SnapshotSchema()
|
||||
schema_lite = Snapshot_lite()
|
||||
schemas_hwmd = {'1.0.0': Snapshot_lite()}
|
||||
devices = []
|
||||
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
||||
for filename, snapshot_json in self.snapshots:
|
||||
self.json_wb = copy.copy(snapshot_json)
|
||||
path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
|
||||
debug = snapshot_json.pop('debug', None)
|
||||
self.version = snapshot_json.get('schema_api')
|
||||
self.uuid = snapshot_json.get('uuid')
|
||||
self.sid = snapshot_json.get('sid')
|
||||
# import pdb; pdb.set_trace()
|
||||
|
||||
if snapshot_json.get('hwmd'):
|
||||
schema_api = snapshot_json.get('schema_api')
|
||||
schema_lite = schemas_hwmd.get(schema_api)
|
||||
if not schema_lite:
|
||||
txt = "Error: No there are schema_api in the snapshot {}"
|
||||
txt = txt.format(self.uuid)
|
||||
raise txt
|
||||
|
||||
if self.is_wb_lite_snapshot(self.version):
|
||||
self.snapshot_json = schema_lite.load(snapshot_json)
|
||||
snapshot_json = ParseSnapshot(self.snapshot_json).snapshot_json
|
||||
# snapshot_json = ParseSnapshotLsHw(self.snapshot_json).snapshot_json
|
||||
snapshot_json = ParseSnapshotLsHw(self.snapshot_json).snapshot_json
|
||||
else:
|
||||
self.version = snapshot_json.get('version')
|
||||
system_uuid = self.get_uuid(debug)
|
||||
|
@ -842,6 +842,9 @@ class ActionFormMixin(FlaskForm):
|
|||
if not self._devices:
|
||||
return False
|
||||
|
||||
if len(devices) > 1 and self.type.data == 'EWaste':
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def generic_validation(self, extra_validators=None):
|
||||
|
@ -858,6 +861,17 @@ class ActionFormMixin(FlaskForm):
|
|||
|
||||
self.populate_obj(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()
|
||||
|
||||
self.devices.data = devices
|
||||
|
@ -1214,7 +1228,6 @@ class TradeForm(ActionFormMixin):
|
|||
or email_to == email_from
|
||||
or g.user.email not in [email_from, email_to]
|
||||
):
|
||||
|
||||
errors = ["If you want confirm, you need a correct email"]
|
||||
self.user_to.errors = errors
|
||||
self.user_from.errors = errors
|
||||
|
@ -1920,7 +1933,6 @@ class UploadPlaceholderForm(FlaskForm):
|
|||
return True
|
||||
|
||||
def save(self, commit=True):
|
||||
|
||||
for device, placeholder_log in self.placeholders:
|
||||
db.session.add(device)
|
||||
db.session.add(placeholder_log)
|
||||
|
@ -1949,7 +1961,6 @@ class EditPlaceholderForm(FlaskForm):
|
|||
return True
|
||||
|
||||
def save(self, commit=True):
|
||||
|
||||
for device in self.placeholders:
|
||||
db.session.add(device)
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@ Create Date: 2023-02-13 18:01:00.092527
|
|||
"""
|
||||
import citext
|
||||
import sqlalchemy as sa
|
||||
from ereuse_devicehub import teal
|
||||
from alembic import context, op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from ereuse_devicehub import teal
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4f33137586dd'
|
||||
down_revision = '93daff872771'
|
||||
|
|
|
@ -11,7 +11,7 @@ from sqlalchemy.dialects import postgresql
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5169765e2653'
|
||||
down_revision = '2f2ef041483a'
|
||||
down_revision = 'a8a86dbd5f51'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
"""add rols to user
|
||||
|
||||
Revision ID: a8a86dbd5f51
|
||||
Revises: 5169765e2653
|
||||
Create Date: 2023-06-14 15:04:03.478157
|
||||
|
||||
"""
|
||||
import citext
|
||||
import sqlalchemy as sa
|
||||
from alembic import context, op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a8a86dbd5f51'
|
||||
down_revision = '2f2ef041483a'
|
||||
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.add_column(
|
||||
'user',
|
||||
sa.Column('rols_dlt', type_=citext.CIText(), nullable=True),
|
||||
schema='common',
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('user', 'rols_dlt', schema='common')
|
|
@ -1,30 +0,0 @@
|
|||
"""add usody in enum software
|
||||
|
||||
Revision ID: be6847b24846
|
||||
Revises: 5169765e2653
|
||||
Create Date: 2023-07-11 14:07:08.887104
|
||||
|
||||
"""
|
||||
from alembic import context, op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'be6847b24846'
|
||||
down_revision = '5169765e2653'
|
||||
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.execute("ALTER TYPE snapshotsoftware ADD VALUE 'UsodyOS'")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# "select e.enumlabel FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'snapshotsoftware'"
|
||||
pass
|
|
@ -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">
|
||||
© 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>
|
|
@ -0,0 +1,300 @@
|
|||
<!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">Basic</h5>
|
||||
<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">
|
||||
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 }}</a>
|
||||
{% else %}
|
||||
- not detected -
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Before Digital Passport (Before Dpp):
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if before_dpp %}
|
||||
<a href="{{ url_for('did.did', id_dpp=before_dpp.key) }}">{{ before_dpp.key }}</a>
|
||||
{% else %}
|
||||
- not detected -
|
||||
{% endif %}
|
||||
</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">
|
||||
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">
|
||||
{% if rol %}
|
||||
{{ device_abstract.serial_number and device_abstract.serial_number.upper() or '- not detected -' }}
|
||||
{% else %}
|
||||
- anonymized -
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</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 -' }}
|
||||
{% 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">
|
||||
© 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>
|
|
@ -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">
|
||||
© 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>
|
|
@ -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">
|
||||
© 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>
|
|
@ -0,0 +1,289 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
import flask
|
||||
import requests
|
||||
from ereuseapi.methods import API
|
||||
from flask import Blueprint
|
||||
from flask import current_app as app
|
||||
from flask import g, render_template, request, session
|
||||
from flask.json import jsonify
|
||||
from flask.views import View
|
||||
|
||||
from ereuse_devicehub import __version__
|
||||
from ereuse_devicehub.modules.dpp.models import Dpp, ALGORITHM
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
did = Blueprint('did', __name__, url_prefix='/did', template_folder='templates')
|
||||
|
||||
|
||||
class DidView(View):
|
||||
methods = ['GET', 'POST']
|
||||
template_name = 'anonymous.html'
|
||||
|
||||
def dispatch_request(self, id_dpp):
|
||||
self.dpp = None
|
||||
self.device = None
|
||||
self.get_ids(id_dpp)
|
||||
|
||||
self.context = {
|
||||
'version': __version__,
|
||||
'oidc': 'oidc' in app.blueprints.keys(),
|
||||
'user': g.user,
|
||||
'path': request.path,
|
||||
'last_dpp': None,
|
||||
'before_dpp': None,
|
||||
'rols': [],
|
||||
'rol': None,
|
||||
}
|
||||
self.get_rols()
|
||||
self.get_rol()
|
||||
self.get_device()
|
||||
self.get_last_dpp()
|
||||
self.get_before_dpp()
|
||||
|
||||
if self.accept_json():
|
||||
return jsonify(self.get_result())
|
||||
|
||||
self.get_manuals()
|
||||
self.get_template()
|
||||
|
||||
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):
|
||||
if 'json' in request.headers.get('Accept', []):
|
||||
return True
|
||||
if "application/json" in request.headers.get("Content-Type", []):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_ids(self, id_dpp):
|
||||
self.id_dpp = None
|
||||
self.chid = id_dpp
|
||||
|
||||
if len(id_dpp.split(":")) == 2:
|
||||
self.id_dpp = id_dpp
|
||||
self.chid = id_dpp.split(':')[0]
|
||||
|
||||
def get_rols(self):
|
||||
rols = session.get('rols')
|
||||
if not g.user.is_authenticated and not rols:
|
||||
return []
|
||||
|
||||
if rols and rols != [('', '')]:
|
||||
self.context['rols'] = rols
|
||||
|
||||
if 'dpp' not in app.blueprints.keys():
|
||||
return []
|
||||
|
||||
if not session.get('token_dlt'):
|
||||
return []
|
||||
|
||||
_role = g.user.get_rols_dlt()
|
||||
role = session.get('iota_abac_attributes', {}).get('role', '')
|
||||
|
||||
if not _role:
|
||||
return []
|
||||
self.context['rols'] = _role
|
||||
return _role
|
||||
|
||||
def get_rol(self):
|
||||
rols = self.context.get('rols', [])
|
||||
rol = len(rols) == 1 and rols[0][0] or None
|
||||
if 'rol' in request.args and not rol:
|
||||
rol = dict(rols).get(request.args.get('rol'))
|
||||
self.context['rol'] = rol
|
||||
|
||||
def get_device(self):
|
||||
if self.id_dpp:
|
||||
self.dpp = Dpp.query.filter_by(key=self.id_dpp).one()
|
||||
device = self.dpp.device
|
||||
else:
|
||||
device = Device.query.filter_by(chid=self.chid, active=True).first()
|
||||
|
||||
if not device:
|
||||
return flask.abort(404)
|
||||
|
||||
placeholder = device.binding or device.placeholder
|
||||
device_abstract = placeholder and placeholder.binding or device
|
||||
device_real = placeholder and placeholder.device or device
|
||||
self.device = device_abstract
|
||||
components = self.device.components
|
||||
if self.dpp:
|
||||
components = self.dpp.snapshot.components
|
||||
|
||||
self.context.update(
|
||||
{
|
||||
'placeholder': placeholder,
|
||||
'device': self.device,
|
||||
'device_abstract': device_abstract,
|
||||
'device_real': device_real,
|
||||
'components': components,
|
||||
}
|
||||
)
|
||||
|
||||
def get_last_dpp(self):
|
||||
dpps = sorted(self.device.dpps, key=lambda x: x.created)
|
||||
self.context['last_dpp'] = dpps and dpps[-1] or ''
|
||||
return self.context['last_dpp']
|
||||
|
||||
def get_before_dpp(self):
|
||||
if not self.dpp:
|
||||
self.context['before_dpp'] = ''
|
||||
return ''
|
||||
|
||||
dpps = sorted(self.device.dpps, key=lambda x: x.created)
|
||||
before_dpp = ''
|
||||
for dpp in dpps:
|
||||
if dpp == self.dpp:
|
||||
break
|
||||
before_dpp = dpp
|
||||
|
||||
self.context['before_dpp'] = before_dpp
|
||||
return before_dpp
|
||||
|
||||
def get_result(self):
|
||||
components = []
|
||||
data = {
|
||||
'document': {},
|
||||
'dpp': self.id_dpp,
|
||||
'algorithm': ALGORITHM,
|
||||
'components': components,
|
||||
'manufacturer DPP': '',
|
||||
}
|
||||
result = {
|
||||
'@context': ['https://ereuse.org/dpp0.json'],
|
||||
'data': data,
|
||||
}
|
||||
|
||||
if self.dpp:
|
||||
data['document'] = self.dpp.snapshot.json_hw
|
||||
last_dpp = self.get_last_dpp()
|
||||
url_last = ''
|
||||
if last_dpp:
|
||||
url_last = 'https://{host}/{did}'.format(
|
||||
did=last_dpp.key, host=app.config.get('HOST')
|
||||
)
|
||||
data['url_last'] = url_last
|
||||
|
||||
for c in self.dpp.snapshot.components:
|
||||
components.append({c.type: c.chid})
|
||||
return result
|
||||
|
||||
dpps = []
|
||||
for d in self.device.dpps:
|
||||
rr = {
|
||||
'dpp': d.key,
|
||||
'document': d.snapshot.json_hw,
|
||||
'algorithm': ALGORITHM,
|
||||
'manufacturer DPP': '',
|
||||
}
|
||||
dpps.append(rr)
|
||||
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'))
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
|
@ -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()
|
|
@ -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"}
|
|
@ -0,0 +1,35 @@
|
|||
"""add api_keys_dlt to user
|
||||
|
||||
Revision ID: 4b7f77f121bf
|
||||
Revises:
|
||||
Create Date: 2022-12-01 10:35:36.795035
|
||||
|
||||
"""
|
||||
import citext
|
||||
import sqlalchemy as sa
|
||||
from alembic import context, op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4b7f77f121bf'
|
||||
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.add_column(
|
||||
'user',
|
||||
sa.Column('api_keys_dlt', type_=citext.CIText(), nullable=True),
|
||||
schema='common',
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('user', 'api_keys_dlt', schema='common')
|
|
@ -0,0 +1,138 @@
|
|||
"""add digital passport dpp
|
||||
|
||||
Revision ID: 8334535d56fa
|
||||
Revises: 4b7f77f121bf
|
||||
Create Date: 2023-01-19 12:01:54.102326
|
||||
|
||||
"""
|
||||
import citext
|
||||
import sqlalchemy as sa
|
||||
from alembic import context, op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8334535d56fa'
|
||||
down_revision = '4b7f77f121bf'
|
||||
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(
|
||||
'proof',
|
||||
sa.Column(
|
||||
'updated',
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||
nullable=False,
|
||||
comment='The last time Devicehub recorded a change for \n this thing.\n ',
|
||||
),
|
||||
sa.Column(
|
||||
'created',
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||
nullable=False,
|
||||
comment='When Devicehub created this.',
|
||||
),
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('type', sa.Unicode(), nullable=False),
|
||||
sa.Column('documentId', 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('device_id', sa.BigInteger(), 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.ForeignKeyConstraint(
|
||||
['action_id'],
|
||||
[f'{get_inv()}.action.id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['device_id'],
|
||||
[f'{get_inv()}.device.id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['issuer_id'],
|
||||
['common.user.id'],
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema=f'{get_inv()}',
|
||||
)
|
||||
# op.create_index(op.f('ix_proof_created'), 'proof', ['created'], unique=False, schema=f'{get_inv()}')
|
||||
# op.create_index(op.f('ix_proof_timestamp'), 'proof', ['timestamp'], unique=False, schema=f'{get_inv()}')
|
||||
op.add_column(
|
||||
'snapshot',
|
||||
sa.Column('phid_dpp', citext.CIText(), nullable=True),
|
||||
schema=f'{get_inv()}',
|
||||
)
|
||||
op.add_column(
|
||||
'snapshot',
|
||||
sa.Column('json_wb', citext.CIText(), nullable=True),
|
||||
schema=f'{get_inv()}',
|
||||
)
|
||||
op.add_column(
|
||||
'snapshot',
|
||||
sa.Column('json_hw', citext.CIText(), nullable=True),
|
||||
schema=f'{get_inv()}',
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'dpp',
|
||||
sa.Column(
|
||||
'updated',
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||
nullable=False,
|
||||
comment='The last time Devicehub recorded a change for \n this thing.\n ',
|
||||
),
|
||||
sa.Column(
|
||||
'created',
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text('CURRENT_TIMESTAMP'),
|
||||
nullable=False,
|
||||
comment='When Devicehub created this.',
|
||||
),
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('documentId', citext.CIText(), nullable=True),
|
||||
sa.Column('documentSignature', citext.CIText(), nullable=True),
|
||||
sa.Column('timestamp', 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('issuer_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['snapshot_id'],
|
||||
[f'{get_inv()}.snapshot.id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['device_id'],
|
||||
[f'{get_inv()}.device.id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['issuer_id'],
|
||||
['common.user.id'],
|
||||
),
|
||||
sa.Column('key', citext.CIText(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
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():
|
||||
op.drop_table('dpp', 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_timestamp'), table_name='proof', schema=f'{get_inv()}')
|
||||
op.drop_column('snapshot', 'phid_dpp', schema=f'{get_inv()}')
|
||||
op.drop_column('snapshot', 'json_wb', schema=f'{get_inv()}')
|
||||
op.drop_column('snapshot', 'json_hw', schema=f'{get_inv()}')
|
|
@ -0,0 +1,117 @@
|
|||
from citext import CIText
|
||||
from flask import g
|
||||
from sortedcontainers import SortedSet
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Sequence, Unicode
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import backref, relationship
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
from ereuse_devicehub.resources.action.models import Action, Snapshot
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
from ereuse_devicehub.teal.db import CASCADE_OWN
|
||||
|
||||
|
||||
ALGORITHM = "sha3-256"
|
||||
|
||||
|
||||
PROOF_ENUM = {
|
||||
'Register': 'Register',
|
||||
'IssueDPP': 'IssueDPP',
|
||||
'proof_of_recycling': 'proof_of_recycling',
|
||||
'Erase': 'Erase',
|
||||
'EWaste': 'EWaste',
|
||||
'Recycled': 'Device_recycled',
|
||||
}
|
||||
|
||||
|
||||
_sorted_proofs = {
|
||||
'order_by': lambda: Proof.created,
|
||||
'collection_class': SortedSet
|
||||
}
|
||||
|
||||
|
||||
class Proof(Thing):
|
||||
id = Column(BigInteger, Sequence('proof_seq'), primary_key=True)
|
||||
id.comment = """The identifier of the device for this database. Used only
|
||||
internally for software; users should not use this."""
|
||||
documentId = Column(CIText(), nullable=True)
|
||||
documentId.comment = "is the uuid of snapshot"
|
||||
documentSignature = Column(CIText(), nullable=True)
|
||||
documentSignature.comment = "is the snapshot.json_hw with the signature of the user"
|
||||
normalizeDoc = Column(CIText(), nullable=True)
|
||||
timestamp = Column(BigInteger, nullable=False)
|
||||
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||
|
||||
issuer_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey(User.id),
|
||||
nullable=False,
|
||||
default=lambda: g.user.id,
|
||||
)
|
||||
issuer = relationship(
|
||||
User,
|
||||
backref=backref('issuered_proofs', lazy=True, collection_class=set),
|
||||
primaryjoin=User.id == issuer_id,
|
||||
)
|
||||
issuer_id.comment = """The user that recorded this proof in the system."""
|
||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
||||
device = relationship(
|
||||
Device,
|
||||
backref=backref('proofs', lazy=True, cascade=CASCADE_OWN),
|
||||
primaryjoin=Device.id == device_id,
|
||||
)
|
||||
|
||||
action_id = Column(UUID(as_uuid=True), ForeignKey(Action.id), nullable=True)
|
||||
action = relationship(
|
||||
Action,
|
||||
backref=backref('proofs', lazy=True),
|
||||
collection_class=OrderedSet,
|
||||
primaryjoin=Action.id == action_id,
|
||||
)
|
||||
|
||||
|
||||
class Dpp(Thing):
|
||||
"""
|
||||
Digital PassPort:
|
||||
It is a type of proof with some field more.
|
||||
Is the official Digital Passport
|
||||
|
||||
"""
|
||||
|
||||
id = Column(BigInteger, Sequence('dpp_seq'), primary_key=True)
|
||||
key = Column(CIText(), nullable=False)
|
||||
key.comment = "chid:phid, (chid it's in device and phid it's in the snapshot)"
|
||||
documentId = Column(CIText(), nullable=True)
|
||||
documentId.comment = "is the uuid of snapshot"
|
||||
documentSignature = Column(CIText(), nullable=True)
|
||||
documentSignature.comment = "is the snapshot.json_hw with the signature of the user"
|
||||
timestamp = Column(BigInteger, nullable=False)
|
||||
|
||||
issuer_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey(User.id),
|
||||
nullable=False,
|
||||
default=lambda: g.user.id,
|
||||
)
|
||||
issuer = relationship(
|
||||
User,
|
||||
backref=backref('issuered_dpp', lazy=True, collection_class=set),
|
||||
primaryjoin=User.id == issuer_id,
|
||||
)
|
||||
issuer_id.comment = """The user that recorded this proof in the system."""
|
||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
||||
device = relationship(
|
||||
Device,
|
||||
backref=backref('dpps', lazy=True, cascade=CASCADE_OWN),
|
||||
primaryjoin=Device.id == device_id,
|
||||
)
|
||||
|
||||
snapshot_id = Column(UUID(as_uuid=True), ForeignKey(Snapshot.id), nullable=False)
|
||||
snapshot = relationship(
|
||||
Snapshot,
|
||||
backref=backref('dpp', lazy=True),
|
||||
collection_class=OrderedSet,
|
||||
primaryjoin=Snapshot.id == snapshot_id,
|
||||
)
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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'))
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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])
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
|
@ -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()
|
|
@ -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"}
|
|
@ -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;")
|
|
@ -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;")
|
|
@ -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)
|
|
@ -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())
|
|
@ -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"],
|
||||
}
|
|
@ -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()
|
|
@ -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()
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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">
|
||||
© 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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -1,15 +1,12 @@
|
|||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import numpy
|
||||
from dmidecode import DMIParse
|
||||
from flask import request
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from ereuse_devicehub.ereuse_utils.nested_lookup import get_nested_dicts_with_key_value
|
||||
from ereuse_devicehub.parser import base2, unit
|
||||
from ereuse_devicehub.parser import base2
|
||||
from ereuse_devicehub.parser.computer import Computer
|
||||
from ereuse_devicehub.parser.models import SnapshotsLog
|
||||
from ereuse_devicehub.resources.action.schemas import Snapshot
|
||||
|
@ -21,31 +18,25 @@ logger = logging.getLogger(__name__)
|
|||
class ParseSnapshot:
|
||||
def __init__(self, snapshot, default="n/a"):
|
||||
self.default = default
|
||||
self.dmidecode_raw = snapshot["hwmd"]["dmidecode"]
|
||||
self.smart_raw = snapshot["hwmd"]["smart"]
|
||||
self.hwinfo_raw = snapshot["hwmd"]["hwinfo"]
|
||||
self.lshw_raw = snapshot["hwmd"]["lshw"]
|
||||
self.lscpi_raw = snapshot["hwmd"]["lspci"]
|
||||
self.sanitize_raw = snapshot.get("sanitize", [])
|
||||
self.dmidecode_raw = snapshot["data"]["dmidecode"]
|
||||
self.smart_raw = snapshot["data"]["smart"]
|
||||
self.hwinfo_raw = snapshot["data"]["hwinfo"]
|
||||
self.device = {"actions": []}
|
||||
self.components = []
|
||||
self.monitors = []
|
||||
|
||||
self.dmi = DMIParse(self.dmidecode_raw)
|
||||
self.smart = self.loads(self.smart_raw)
|
||||
self.lshw = self.loads(self.lshw_raw)
|
||||
self.hwinfo = self.parse_hwinfo()
|
||||
|
||||
self.set_computer()
|
||||
self.get_hwinfo_monitors()
|
||||
self.set_basic_datas()
|
||||
self.set_components()
|
||||
self.snapshot_json = {
|
||||
"device": self.device,
|
||||
"software": "UsodyOS",
|
||||
"software": "Workbench",
|
||||
"components": self.components,
|
||||
"uuid": snapshot['uuid'],
|
||||
"version": snapshot['version'],
|
||||
"settings_version": snapshot['settings_version'],
|
||||
"type": snapshot['type'],
|
||||
"version": "14.0.0",
|
||||
"endTime": snapshot["timestamp"],
|
||||
"elapsed": 1,
|
||||
"sid": snapshot["sid"],
|
||||
|
@ -54,7 +45,7 @@ class ParseSnapshot:
|
|||
def get_snapshot(self):
|
||||
return Snapshot().load(self.snapshot_json)
|
||||
|
||||
def set_computer(self):
|
||||
def set_basic_datas(self):
|
||||
self.device['manufacturer'] = self.dmi.manufacturer()
|
||||
self.device['model'] = self.dmi.model()
|
||||
self.device['serialNumber'] = self.dmi.serial_number()
|
||||
|
@ -62,24 +53,17 @@ class ParseSnapshot:
|
|||
self.device['sku'] = self.get_sku()
|
||||
self.device['version'] = self.get_version()
|
||||
self.device['system_uuid'] = self.get_uuid()
|
||||
self.device['family'] = self.get_family()
|
||||
self.device['chassis'] = self.get_chassis_dh()
|
||||
|
||||
def set_components(self):
|
||||
self.get_cpu()
|
||||
self.get_ram()
|
||||
self.get_mother_board()
|
||||
self.get_graphic()
|
||||
self.get_data_storage()
|
||||
self.get_display()
|
||||
self.get_sound_card()
|
||||
self.get_networks()
|
||||
|
||||
def get_cpu(self):
|
||||
# TODO @cayop generation, brand and address not exist in dmidecode
|
||||
for cpu in self.dmi.get('Processor'):
|
||||
serial = cpu.get('Serial Number')
|
||||
if serial == 'Not Specified' or not serial:
|
||||
serial = cpu.get('ID').replace(' ', '')
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
|
@ -89,20 +73,16 @@ class ParseSnapshot:
|
|||
"model": cpu.get('Version'),
|
||||
"threads": int(cpu.get('Thread Count', 1)),
|
||||
"manufacturer": cpu.get('Manufacturer'),
|
||||
"serialNumber": serial,
|
||||
"generation": None,
|
||||
"brand": cpu.get('Family'),
|
||||
"address": self.get_cpu_address(cpu),
|
||||
"serialNumber": cpu.get('Serial Number'),
|
||||
"generation": cpu.get('Generation'),
|
||||
"brand": cpu.get('Brand'),
|
||||
"address": cpu.get('Address'),
|
||||
}
|
||||
)
|
||||
|
||||
def get_ram(self):
|
||||
# TODO @cayop format and model not exist in dmidecode
|
||||
for ram in self.dmi.get("Memory Device"):
|
||||
if ram.get('size') == 'No Module Installed':
|
||||
continue
|
||||
if not ram.get("Speed"):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
|
@ -111,13 +91,14 @@ class ParseSnapshot:
|
|||
"speed": self.get_ram_speed(ram),
|
||||
"manufacturer": ram.get("Manufacturer", self.default),
|
||||
"serialNumber": ram.get("Serial Number", self.default),
|
||||
"interface": ram.get("Type", "DDR"),
|
||||
"format": ram.get("Form Factor", "DIMM"),
|
||||
"interface": self.get_ram_type(ram),
|
||||
"format": self.get_ram_format(ram),
|
||||
"model": ram.get("Part Number", self.default),
|
||||
}
|
||||
)
|
||||
|
||||
def get_mother_board(self):
|
||||
# TODO @cayop model, not exist in dmidecode
|
||||
for moder_board in self.dmi.get("Baseboard"):
|
||||
self.components.append(
|
||||
{
|
||||
|
@ -127,232 +108,17 @@ class ParseSnapshot:
|
|||
"serialNumber": moder_board.get("Serial Number"),
|
||||
"manufacturer": moder_board.get("Manufacturer"),
|
||||
"biosDate": self.get_bios_date(),
|
||||
# "firewire": self.get_firmware(),
|
||||
"ramMaxSize": self.get_max_ram_size(),
|
||||
"ramSlots": len(self.dmi.get("Memory Device")),
|
||||
"slots": self.get_ram_slots(),
|
||||
"model": moder_board.get("Product Name"),
|
||||
"firewire": self.get_firmware_num(),
|
||||
"pcmcia": self.get_pcmcia_num(),
|
||||
"serial": self.get_serial_num(),
|
||||
"model": moder_board.get("Product Name"), # ??
|
||||
"pcmcia": self.get_pcmcia_num(), # ??
|
||||
"serial": self.get_serial_num(), # ??
|
||||
"usb": self.get_usb_num(),
|
||||
}
|
||||
)
|
||||
|
||||
def get_graphic(self):
|
||||
nodes = get_nested_dicts_with_key_value(self.lshw, 'class', 'display')
|
||||
for c in nodes:
|
||||
if not c['configuration'].get('driver', None):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "GraphicCard",
|
||||
"memory": self.get_memory_video(c),
|
||||
"manufacturer": c.get("vendor", self.default),
|
||||
"model": c.get("product", self.default),
|
||||
"serialNumber": c.get("serial", self.default),
|
||||
}
|
||||
)
|
||||
|
||||
def get_memory_video(self, c):
|
||||
# get info of lspci
|
||||
# pci_id = c['businfo'].split('@')[1]
|
||||
# lspci.get(pci_id) | grep size
|
||||
# lspci -v -s 00:02.0
|
||||
return None
|
||||
|
||||
def get_data_storage(self):
|
||||
for sm in self.smart:
|
||||
if sm.get('smartctl', {}).get('exit_status') == 1:
|
||||
continue
|
||||
model = sm.get('model_name')
|
||||
manufacturer = None
|
||||
if model and len(model.split(" ")) > 1:
|
||||
mm = model.split(" ")
|
||||
model = mm[-1]
|
||||
manufacturer = " ".join(mm[:-1])
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": self.sanitize(sm),
|
||||
"type": self.get_data_storage_type(sm),
|
||||
"model": model,
|
||||
"manufacturer": manufacturer,
|
||||
"serialNumber": sm.get('serial_number'),
|
||||
"size": self.get_data_storage_size(sm),
|
||||
"variant": sm.get("firmware_version"),
|
||||
"interface": self.get_data_storage_interface(sm),
|
||||
}
|
||||
)
|
||||
|
||||
def sanitize(self, disk):
|
||||
disk_sanitize = None
|
||||
# import pdb; pdb.set_trace()
|
||||
for d in self.sanitize_raw:
|
||||
s = d.get('device_info', {}).get('export_data', {})
|
||||
s = s.get('block', {}).get('serial')
|
||||
if s == disk.get('serial_number'):
|
||||
disk_sanitize = d
|
||||
break
|
||||
if not disk_sanitize:
|
||||
return []
|
||||
|
||||
steps = []
|
||||
step_type = 'EraseBasic'
|
||||
if disk.get('name') == 'Baseline Cryptographic':
|
||||
step_type = 'EraseCrypto'
|
||||
|
||||
if disk.get('type') == 'EraseCrypto':
|
||||
step_type = 'EraseCrypto'
|
||||
|
||||
erase = {
|
||||
'type': step_type,
|
||||
'severity': disk_sanitize['severity'].name,
|
||||
'steps': steps,
|
||||
'startTime': None,
|
||||
'endTime': None,
|
||||
}
|
||||
|
||||
for step in disk_sanitize.get('steps', []):
|
||||
steps.append(
|
||||
{
|
||||
'severity': step['severity'].name,
|
||||
'startTime': step['start_time'].isoformat(),
|
||||
'endTime': step['end_time'].isoformat(),
|
||||
'type': 'StepRandom',
|
||||
}
|
||||
)
|
||||
|
||||
erase['endTime'] = step['end_time'].isoformat()
|
||||
if not erase['startTime']:
|
||||
erase['startTime'] = step['start_time'].isoformat()
|
||||
return [erase]
|
||||
|
||||
def get_networks(self):
|
||||
nodes = get_nested_dicts_with_key_value(self.lshw, 'class', 'network')
|
||||
for c in nodes:
|
||||
capacity = c.get('capacity')
|
||||
units = c.get('units')
|
||||
speed = None
|
||||
if capacity and units:
|
||||
speed = unit.Quantity(capacity, units).to('Mbit/s').m
|
||||
wireless = bool(c.get('configuration', {}).get('wireless', False))
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "NetworkAdapter",
|
||||
"model": c.get('product'),
|
||||
"manufacturer": c.get('vendor'),
|
||||
"serialNumber": c.get('serial'),
|
||||
"speed": speed,
|
||||
"variant": c.get('version', 1),
|
||||
"wireless": wireless,
|
||||
}
|
||||
)
|
||||
|
||||
def get_sound_card(self):
|
||||
nodes = get_nested_dicts_with_key_value(self.lshw, 'class', 'multimedia')
|
||||
for c in nodes:
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "SoundCard",
|
||||
"model": c.get('product'),
|
||||
"manufacturer": c.get('vendor'),
|
||||
"serialNumber": c.get('serial'),
|
||||
}
|
||||
)
|
||||
|
||||
def get_display(self): # noqa: C901
|
||||
TECHS = 'CRT', 'TFT', 'LED', 'PDP', 'LCD', 'OLED', 'AMOLED'
|
||||
|
||||
for c in self.monitors:
|
||||
resolution_width, resolution_height = (None,) * 2
|
||||
refresh, serial, model, manufacturer, size = (None,) * 5
|
||||
year, week, production_date = (None,) * 3
|
||||
|
||||
for x in c:
|
||||
if "Vendor: " in x:
|
||||
manufacturer = x.split('Vendor: ')[-1].strip()
|
||||
if "Model: " in x:
|
||||
model = x.split('Model: ')[-1].strip()
|
||||
if "Serial ID: " in x:
|
||||
serial = x.split('Serial ID: ')[-1].strip()
|
||||
if " Resolution: " in x:
|
||||
rs = x.split(' Resolution: ')[-1].strip()
|
||||
if 'x' in rs:
|
||||
resolution_width, resolution_height = [
|
||||
int(r) for r in rs.split('x')
|
||||
]
|
||||
if "Frequencies: " in x:
|
||||
try:
|
||||
refresh = int(float(x.split(',')[-1].strip()[:-3]))
|
||||
except Exception:
|
||||
pass
|
||||
if 'Year of Manufacture' in x:
|
||||
year = x.split(': ')[1]
|
||||
|
||||
if 'Week of Manufacture' in x:
|
||||
week = x.split(': ')[1]
|
||||
|
||||
if "Size: " in x:
|
||||
size = self.get_size_monitor(x)
|
||||
technology = next((t for t in TECHS if t in c[0]), None)
|
||||
|
||||
if year and week:
|
||||
d = '{} {} 0'.format(year, week)
|
||||
production_date = datetime.strptime(d, '%Y %W %w').isoformat()
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": "Display",
|
||||
"model": model,
|
||||
"manufacturer": manufacturer,
|
||||
"serialNumber": serial,
|
||||
'size': size,
|
||||
'resolutionWidth': resolution_width,
|
||||
'resolutionHeight': resolution_height,
|
||||
"productionDate": production_date,
|
||||
'technology': technology,
|
||||
'refreshRate': refresh,
|
||||
}
|
||||
)
|
||||
|
||||
def get_hwinfo_monitors(self):
|
||||
for c in self.hwinfo:
|
||||
monitor = None
|
||||
external = None
|
||||
for x in c:
|
||||
if 'Hardware Class: monitor' in x:
|
||||
monitor = c
|
||||
if 'Driver Info' in x:
|
||||
external = c
|
||||
|
||||
if monitor and not external:
|
||||
self.monitors.append(c)
|
||||
|
||||
def get_size_monitor(self, x):
|
||||
i = 1 / 25.4
|
||||
t = x.split('Size: ')[-1].strip()
|
||||
tt = t.split('mm')
|
||||
if not tt:
|
||||
return 0
|
||||
sizes = tt[0].strip()
|
||||
if 'x' not in sizes:
|
||||
return 0
|
||||
w, h = [int(x) for x in sizes.split('x')]
|
||||
return numpy.sqrt(w**2 + h**2) * i
|
||||
|
||||
def get_cpu_address(self, cpu):
|
||||
default = 64
|
||||
for ch in self.lshw.get('children', []):
|
||||
for c in ch.get('children', []):
|
||||
if c['class'] == 'processor':
|
||||
return c.get('width', default)
|
||||
return default
|
||||
|
||||
def get_usb_num(self):
|
||||
return len(
|
||||
[
|
||||
|
@ -371,15 +137,6 @@ class ParseSnapshot:
|
|||
]
|
||||
)
|
||||
|
||||
def get_firmware_num(self):
|
||||
return len(
|
||||
[
|
||||
u
|
||||
for u in self.dmi.get("Port Connector")
|
||||
if "FIRMWARE" in u.get("Port Type", "").upper()
|
||||
]
|
||||
)
|
||||
|
||||
def get_pcmcia_num(self):
|
||||
return len(
|
||||
[
|
||||
|
@ -410,22 +167,23 @@ class ParseSnapshot:
|
|||
return slots
|
||||
|
||||
def get_ram_size(self, ram):
|
||||
try:
|
||||
memory = ram.get("Size", "0")
|
||||
memory = memory.split(' ')
|
||||
if len(memory) > 1:
|
||||
size = int(memory[0])
|
||||
units = memory[1]
|
||||
return base2.Quantity(size, units).to('MiB').m
|
||||
return int(size.split(" ")[0])
|
||||
except Exception as err:
|
||||
logger.error("get_ram_size error: {}".format(err))
|
||||
return 0
|
||||
size = ram.get("Size", "0")
|
||||
return int(size.split(" ")[0])
|
||||
|
||||
def get_ram_speed(self, ram):
|
||||
size = ram.get("Speed", "0")
|
||||
return int(size.split(" ")[0])
|
||||
|
||||
def get_ram_type(self, ram):
|
||||
TYPES = {'ddr', 'sdram', 'sodimm'}
|
||||
for t in TYPES:
|
||||
if t in ram.get("Type", "DDR"):
|
||||
return t
|
||||
|
||||
def get_ram_format(self, ram):
|
||||
channel = ram.get("Locator", "DIMM")
|
||||
return 'SODIMM' if 'SODIMM' in channel else 'DIMM'
|
||||
|
||||
def get_cpu_speed(self, cpu):
|
||||
speed = cpu.get('Max Speed', "0")
|
||||
return float(speed.split(" ")[0]) / 1024
|
||||
|
@ -437,13 +195,10 @@ class ParseSnapshot:
|
|||
return self.dmi.get("System")[0].get("Version", self.default)
|
||||
|
||||
def get_uuid(self):
|
||||
return self.dmi.get("System")[0].get("UUID", '')
|
||||
|
||||
def get_family(self):
|
||||
return self.dmi.get("System")[0].get("Family", '')
|
||||
return self.dmi.get("System")[0].get("UUID", self.default)
|
||||
|
||||
def get_chassis(self):
|
||||
return self.dmi.get("Chassis")[0].get("Type", '_virtual')
|
||||
return self.dmi.get("Chassis")[0].get("Type", self.default)
|
||||
|
||||
def get_type(self):
|
||||
chassis_type = self.get_chassis()
|
||||
|
@ -483,30 +238,26 @@ class ParseSnapshot:
|
|||
return k
|
||||
return self.default
|
||||
|
||||
def get_chassis_dh(self):
|
||||
CHASSIS_DH = {
|
||||
'Tower': {'desktop', 'low-profile', 'tower', 'server'},
|
||||
'Docking': {'docking'},
|
||||
'AllInOne': {'all-in-one'},
|
||||
'Microtower': {'mini-tower', 'space-saving', 'mini'},
|
||||
'PizzaBox': {'pizzabox'},
|
||||
'Lunchbox': {'lunchbox'},
|
||||
'Stick': {'stick'},
|
||||
'Netbook': {'notebook', 'sub-notebook'},
|
||||
'Handheld': {'handheld'},
|
||||
'Laptop': {'portable', 'laptop'},
|
||||
'Convertible': {'convertible'},
|
||||
'Detachable': {'detachable'},
|
||||
'Tablet': {'tablet'},
|
||||
'Virtual': {'_virtual'},
|
||||
}
|
||||
def get_data_storage(self):
|
||||
|
||||
chassis = self.get_chassis()
|
||||
lower_type = chassis.lower()
|
||||
for k, v in CHASSIS_DH.items():
|
||||
if lower_type in v:
|
||||
return k
|
||||
return self.default
|
||||
for sm in self.smart:
|
||||
model = sm.get('model_name')
|
||||
manufacturer = None
|
||||
if len(model.split(" ")) == 2:
|
||||
manufacturer, model = model.split(" ")
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"type": self.get_data_storage_type(sm),
|
||||
"model": model,
|
||||
"manufacturer": manufacturer,
|
||||
"serialNumber": sm.get('serial_number'),
|
||||
"size": self.get_data_storage_size(sm),
|
||||
"variant": sm.get("firmware_version"),
|
||||
"interface": self.get_data_storage_interface(sm),
|
||||
}
|
||||
)
|
||||
|
||||
def get_data_storage_type(self, x):
|
||||
# TODO @cayop add more SSDS types
|
||||
|
@ -514,27 +265,45 @@ class ParseSnapshot:
|
|||
SSD = 'SolidStateDrive'
|
||||
HDD = 'HardDrive'
|
||||
type_dev = x.get('device', {}).get('type')
|
||||
trim = x.get('trim', {}).get("supported") in [True, "true"]
|
||||
return SSD if type_dev in SSDS or trim else HDD
|
||||
return SSD if type_dev in SSDS else HDD
|
||||
|
||||
def get_data_storage_interface(self, x):
|
||||
interface = x.get('device', {}).get('protocol', 'ATA')
|
||||
try:
|
||||
DataStorageInterface(interface.upper())
|
||||
except ValueError as err:
|
||||
txt = "Sid: {}, interface {} is not in DataStorageInterface Enum".format(
|
||||
self.sid, interface
|
||||
)
|
||||
self.errors("{}".format(err))
|
||||
self.errors(txt, severity=Severity.Warning)
|
||||
return "ATA"
|
||||
return x.get('device', {}).get('protocol', 'ATA')
|
||||
|
||||
def get_data_storage_size(self, x):
|
||||
total_capacity = x.get('user_capacity', {}).get('bytes')
|
||||
if not total_capacity:
|
||||
return 1
|
||||
type_dev = x.get('device', {}).get('type')
|
||||
total_capacity = "{type}_total_capacity".format(type=type_dev)
|
||||
# convert bytes to Mb
|
||||
return total_capacity / 1024**2
|
||||
return x.get(total_capacity) / 1024**2
|
||||
|
||||
def get_networks(self):
|
||||
hw_class = " Hardware Class: "
|
||||
mac = " Permanent HW Address: "
|
||||
model = " Model: "
|
||||
wireless = "wireless"
|
||||
|
||||
for line in self.hwinfo:
|
||||
iface = {
|
||||
"variant": "1",
|
||||
"actions": [],
|
||||
"speed": 100.0,
|
||||
"type": "NetworkAdapter",
|
||||
"wireless": False,
|
||||
"manufacturer": "Ethernet",
|
||||
}
|
||||
for y in line:
|
||||
if hw_class in y and not y.split(hw_class)[1] == 'network':
|
||||
break
|
||||
|
||||
if mac in y:
|
||||
iface["serialNumber"] = y.split(mac)[1]
|
||||
if model in y:
|
||||
iface["model"] = y.split(model)[1]
|
||||
if wireless in y:
|
||||
iface["wireless"] = True
|
||||
|
||||
if iface.get("serialNumber"):
|
||||
self.components.append(iface)
|
||||
|
||||
def parse_hwinfo(self):
|
||||
hw_blocks = self.hwinfo_raw.split("\n\n")
|
||||
|
@ -545,21 +314,6 @@ class ParseSnapshot:
|
|||
return json.loads(x)
|
||||
return x
|
||||
|
||||
def errors(self, txt=None, severity=Severity.Error):
|
||||
if not txt:
|
||||
return self._errors
|
||||
|
||||
logger.error(txt)
|
||||
self._errors.append(txt)
|
||||
error = SnapshotsLog(
|
||||
description=txt,
|
||||
snapshot_uuid=self.uuid,
|
||||
severity=severity,
|
||||
sid=self.sid,
|
||||
version=self.version,
|
||||
)
|
||||
error.save()
|
||||
|
||||
|
||||
class ParseSnapshotLsHw:
|
||||
def __init__(self, snapshot, default="n/a"):
|
||||
|
@ -567,10 +321,10 @@ class ParseSnapshotLsHw:
|
|||
self.uuid = snapshot.get("uuid")
|
||||
self.sid = snapshot.get("sid")
|
||||
self.version = str(snapshot.get("version"))
|
||||
self.dmidecode_raw = snapshot["hwmd"]["dmidecode"]
|
||||
self.smart = snapshot["hwmd"]["smart"]
|
||||
self.hwinfo_raw = snapshot["hwmd"]["hwinfo"]
|
||||
self.lshw = snapshot["hwmd"]["lshw"]
|
||||
self.dmidecode_raw = snapshot["data"]["dmidecode"]
|
||||
self.smart = snapshot["data"]["smart"]
|
||||
self.hwinfo_raw = snapshot["data"]["hwinfo"]
|
||||
self.lshw = snapshot["data"]["lshw"]
|
||||
self.device = {"actions": []}
|
||||
self.components = []
|
||||
self.components_obj = []
|
||||
|
@ -647,11 +401,6 @@ class ParseSnapshotLsHw:
|
|||
|
||||
def get_ram(self):
|
||||
for ram in self.dmi.get("Memory Device"):
|
||||
if ram.get('size') == 'No Module Installed':
|
||||
continue
|
||||
if not ram.get("Speed"):
|
||||
continue
|
||||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
|
@ -725,6 +474,7 @@ class ParseSnapshotLsHw:
|
|||
return dmi_uuid
|
||||
|
||||
def get_data_storage(self):
|
||||
|
||||
for sm in self.smart:
|
||||
if sm.get('smartctl', {}).get('exit_status') == 1:
|
||||
continue
|
||||
|
@ -737,7 +487,7 @@ class ParseSnapshotLsHw:
|
|||
|
||||
self.components.append(
|
||||
{
|
||||
"actions": [],
|
||||
"actions": [self.get_test_data_storage(sm)],
|
||||
"type": self.get_data_storage_type(sm),
|
||||
"model": model,
|
||||
"manufacturer": manufacturer,
|
||||
|
|
|
@ -1,94 +1,37 @@
|
|||
from datetime import datetime
|
||||
|
||||
from flask import current_app as app
|
||||
from marshmallow import Schema as MarshmallowSchema
|
||||
from marshmallow import ValidationError, pre_load, validates_schema
|
||||
from marshmallow.fields import DateTime, Dict, Integer, List, Nested, String
|
||||
from marshmallow_enum import EnumField
|
||||
from marshmallow import ValidationError, validates_schema
|
||||
from marshmallow.fields import Dict, List, Nested, String
|
||||
|
||||
from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
||||
# from marshmallow_enum import EnumField
|
||||
|
||||
|
||||
class Snapshot_lite_data(MarshmallowSchema):
|
||||
hwmd_version = String(required=True)
|
||||
lshw = Dict(required=True)
|
||||
dmidecode = String(required=True)
|
||||
lspci = String(required=True)
|
||||
hwinfo = String(required=True)
|
||||
smart = List(Dict(), required=False)
|
||||
|
||||
|
||||
class Test(MarshmallowSchema):
|
||||
type = String(required=True)
|
||||
|
||||
|
||||
class Steps(MarshmallowSchema):
|
||||
num = Integer(data_key='step', required=True)
|
||||
start_time = DateTime(data_key='date_init', required=False)
|
||||
end_time = DateTime(data_key='date_end', required=False)
|
||||
severity = EnumField(Severity)
|
||||
|
||||
@pre_load
|
||||
def preload_datas(self, data: dict):
|
||||
# import pdb; pdb.set_trace()
|
||||
data['severity'] = Severity.Info.name
|
||||
data.pop('duration', None)
|
||||
data.pop('commands', None)
|
||||
|
||||
if not data.pop('success', False):
|
||||
data['severity'] = Severity.Error.name
|
||||
|
||||
if data.get('date_init'):
|
||||
data['date_init'] = datetime.fromtimestamp(data['date_init']).isoformat()
|
||||
|
||||
if data.get('date_end'):
|
||||
data['date_end'] = datetime.fromtimestamp(data['date_end']).isoformat()
|
||||
else:
|
||||
data['date_end'] = data['date_init']
|
||||
|
||||
|
||||
class Sanitize(MarshmallowSchema):
|
||||
steps = Nested(Steps, many=True, required=True, data_key='erasure_steps')
|
||||
validation = Dict()
|
||||
device_info = Dict()
|
||||
method = Dict(required=True)
|
||||
sanitize_version = String()
|
||||
severity = EnumField(Severity, required=True)
|
||||
|
||||
@pre_load
|
||||
def preload_datas(self, data: dict):
|
||||
data['severity'] = Severity.Info.name
|
||||
|
||||
if not data.pop('result', False):
|
||||
data['severity'] = Severity.Error.name
|
||||
smart = List(Dict(), required=True)
|
||||
lshw = Dict(required=True)
|
||||
lspci = String(required=True)
|
||||
|
||||
|
||||
class Snapshot_lite(Thing):
|
||||
uuid = String(required=True)
|
||||
version = String(required=True)
|
||||
schema_api = String(required=True)
|
||||
software = EnumField(
|
||||
SnapshotSoftware,
|
||||
required=True,
|
||||
description='The software that generated this Snapshot.',
|
||||
)
|
||||
software = String(required=True)
|
||||
sid = String(required=True)
|
||||
type = String(required=True)
|
||||
timestamp = String(required=True)
|
||||
settings_version = String(required=False)
|
||||
hwmd = Nested(Snapshot_lite_data, required=True)
|
||||
tests = Nested(Test, many=True, collection_class=list, required=False)
|
||||
sanitize = Nested(Sanitize, many=True, collection_class=list, required=False)
|
||||
data = Nested(Snapshot_lite_data, required=True)
|
||||
|
||||
@validates_schema
|
||||
def validate_workbench_version(self, data: dict):
|
||||
if data['schema_api'] not in app.config['SCHEMA_HWMD']:
|
||||
if data['schema_api'] not in app.config['SCHEMA_WORKBENCH']:
|
||||
raise ValidationError(
|
||||
'Min. supported Workbench version is '
|
||||
'{} but yours is {}.'.format(
|
||||
app.config['SCHEMA_HWMD'][0], data['version']
|
||||
app.config['SCHEMA_WORKBENCH'][0], data['version']
|
||||
),
|
||||
field_names=['version'],
|
||||
)
|
||||
|
|
|
@ -216,6 +216,16 @@ class ReadyDef(ActionDef):
|
|||
SCHEMA = schemas.Ready
|
||||
|
||||
|
||||
class EWasteDef(ActionDef):
|
||||
VIEW = None
|
||||
SCHEMA = schemas.EWaste
|
||||
|
||||
|
||||
class RecycledDef(ActionDef):
|
||||
VIEW = None
|
||||
SCHEMA = schemas.Recycled
|
||||
|
||||
|
||||
class RecyclingDef(ActionDef):
|
||||
VIEW = None
|
||||
SCHEMA = schemas.Recycling
|
||||
|
|
|
@ -11,10 +11,14 @@ Within the above general classes are subclasses in A order.
|
|||
"""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from collections import Iterable
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import ROUND_HALF_EVEN, ROUND_UP, Decimal
|
||||
from operator import itemgetter
|
||||
from typing import Optional, Set, Union
|
||||
from uuid import uuid4
|
||||
|
||||
|
@ -22,8 +26,9 @@ import inflection
|
|||
from boltons import urlutils
|
||||
from citext import CIText
|
||||
from dateutil.tz import tzutc
|
||||
from ereuseapi.methods import API
|
||||
from flask import current_app as app
|
||||
from flask import g
|
||||
from flask import g, session
|
||||
from sortedcontainers import SortedSet
|
||||
from sqlalchemy import JSON, BigInteger, Boolean, CheckConstraint, Column
|
||||
from sqlalchemy import Enum as DBEnum
|
||||
|
@ -71,6 +76,7 @@ from ereuse_devicehub.resources.enums import (
|
|||
RatingRange,
|
||||
Severity,
|
||||
SnapshotSoftware,
|
||||
StatusCode,
|
||||
TestDataStorageLength,
|
||||
)
|
||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||
|
@ -479,7 +485,7 @@ class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|||
def get_phid(self):
|
||||
"""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
|
||||
Ereased
|
||||
Ereased.
|
||||
"""
|
||||
if self.snapshot:
|
||||
return self.snapshot.device.phid()
|
||||
|
@ -487,6 +493,66 @@ class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|||
return self.parent.phid()
|
||||
return ''
|
||||
|
||||
def connect_api(self):
|
||||
if 'dpp' not in app.blueprints.keys() or not self.snapshot:
|
||||
return
|
||||
|
||||
if not session.get('token_dlt'):
|
||||
return
|
||||
|
||||
token_dlt = session.get('token_dlt')
|
||||
api_dlt = app.config.get('API_DLT')
|
||||
if not token_dlt or not api_dlt:
|
||||
return
|
||||
|
||||
return API(api_dlt, token_dlt, "ethereum")
|
||||
|
||||
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
|
||||
docHash = self.snapshot.phid_dpp
|
||||
docHashAlgorithm = ALGORITHM
|
||||
proof_type = PROOF_ENUM['Erase']
|
||||
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
|
||||
|
||||
if result['Status'] == StatusCode.Success.value:
|
||||
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
|
||||
if not timestamp:
|
||||
return
|
||||
|
||||
d = {
|
||||
"type": PROOF_ENUM['Erase'],
|
||||
"device": self.device,
|
||||
"action": self.snapshot,
|
||||
"documentId": self.snapshot.id,
|
||||
"timestamp": timestamp,
|
||||
"issuer_id": g.user.id,
|
||||
"documentSignature": self.snapshot.phid_dpp,
|
||||
"normalizeDoc": self.snapshot.json_hw,
|
||||
}
|
||||
proof = Proof(**d)
|
||||
db.session.add(proof)
|
||||
|
||||
def get_public_name(self):
|
||||
return "Basic"
|
||||
|
||||
|
@ -523,14 +589,11 @@ class EraseSectors(EraseBasic):
|
|||
def get_public_name(self):
|
||||
steps_random = 0
|
||||
steps_zeros = 0
|
||||
steps_encrypted = 0
|
||||
for s in self.steps:
|
||||
if s.type == 'StepRandom':
|
||||
steps_random += 1
|
||||
if s.type == 'StepZero':
|
||||
steps_zeros += 1
|
||||
if s.type == 'StepEncrypted':
|
||||
steps_encrypted += 1
|
||||
|
||||
if steps_zeros == 0 and steps_random == 1:
|
||||
return "Basic"
|
||||
|
@ -654,10 +717,6 @@ class StepRandom(Step):
|
|||
pass
|
||||
|
||||
|
||||
class StepEncrypted(Step):
|
||||
pass
|
||||
|
||||
|
||||
class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
||||
"""The Snapshot sets the physical information of the device (S/N, model...)
|
||||
and updates it with erasures, benchmarks, ratings, and tests; updates the
|
||||
|
@ -747,7 +806,7 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|||
"""
|
||||
|
||||
uuid = Column(UUID(as_uuid=True), unique=True)
|
||||
version = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||
version = Column(StrictVersionType(STR_SM_SIZE), nullable=False)
|
||||
software = Column(DBEnum(SnapshotSoftware), nullable=False)
|
||||
elapsed = Column(Interval)
|
||||
elapsed.comment = """For Snapshots made with Workbench, the total amount
|
||||
|
@ -756,6 +815,51 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|||
sid = Column(CIText(), nullable=True)
|
||||
settings_version = Column(CIText(), nullable=True)
|
||||
is_server_erase = Column(Boolean(), nullable=True)
|
||||
json_wb = Column(CIText(), nullable=False)
|
||||
json_wb.comment = "original json of the workbench"
|
||||
json_hw = Column(CIText(), nullable=False)
|
||||
json_hw.comment = (
|
||||
"json with alphabetic ordered of the hardware than exist in json_wb"
|
||||
)
|
||||
phid_dpp = Column(CIText(), nullable=False)
|
||||
phid_dpp.comment = "hash of json_hw this with the chid if the device conform the DPP, (Digital PassPort)"
|
||||
|
||||
def create_json_hw(self, json_wb):
|
||||
"""
|
||||
Create a json with the hardware without actions of the original json, (json_wb).
|
||||
This json need have an alphabetic order.
|
||||
Next is necessary create a hash of this json and put it intu phid field.
|
||||
And last save in text the correct json_wb and json_hw in the respective fields
|
||||
"""
|
||||
if not json_wb:
|
||||
return
|
||||
|
||||
json_hw = {}
|
||||
json_wb = copy.copy(json_wb)
|
||||
|
||||
if json_wb.get('device', {}).get('system_uuid'):
|
||||
system_uuid = str(json_wb['device']['system_uuid'])
|
||||
json_wb['device']['system_uuid'] = system_uuid
|
||||
|
||||
for k, v in json_wb.items():
|
||||
if k == 'device':
|
||||
json_hw['device'] = copy.copy(v)
|
||||
json_hw['device'].pop('actions', None)
|
||||
json_hw['device'].pop('actions_one', None)
|
||||
if k == 'components':
|
||||
components = []
|
||||
for component in v:
|
||||
c = component
|
||||
c.pop('actions', None)
|
||||
c.pop('actions_one', None)
|
||||
components.append(c)
|
||||
# if 'manufacturer', 'model', 'serialNumber' key filter broken'
|
||||
# key_filter = itemgetter('type', 'manufacturer', 'model', 'serialNumber')
|
||||
key_filter = itemgetter('type')
|
||||
json_hw['components'] = sorted(components, key=key_filter)
|
||||
self.json_wb = json.dumps(json_wb)
|
||||
self.json_hw = json.dumps(json_hw)
|
||||
self.phid_dpp = hashlib.sha3_256(self.json_hw.encode('utf-8')).hexdigest()
|
||||
|
||||
def get_last_lifetimes(self):
|
||||
"""We get the lifetime and serial_number of the first disk"""
|
||||
|
@ -780,7 +884,6 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|||
return hdds
|
||||
|
||||
def get_new_device(self):
|
||||
|
||||
if not self.device:
|
||||
return ''
|
||||
|
||||
|
@ -792,6 +895,47 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
|||
snapshots.append(s)
|
||||
return snapshots and 'update' or 'new_device'
|
||||
|
||||
def register_passport_dlt(self):
|
||||
if 'dpp' not in app.blueprints.keys() or not self.device.hid:
|
||||
return
|
||||
|
||||
from ereuse_devicehub.modules.dpp.models import Dpp, ALGORITHM
|
||||
|
||||
dpp = "{chid}:{phid}".format(chid=self.device.chid, phid=self.phid_dpp)
|
||||
# '54f431688524825f00d6fb786d5211e642e3d4564187f9ad23627ec9d313f17c'
|
||||
if Dpp.query.filter_by(key=dpp).all():
|
||||
return
|
||||
|
||||
if not session.get('token_dlt'):
|
||||
return
|
||||
|
||||
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")
|
||||
docSig = self.phid_dpp
|
||||
|
||||
result = api.issue_passport(dpp, ALGORITHM, docSig, dh_instance)
|
||||
|
||||
if result['Status'] is not StatusCode.Success.value:
|
||||
return
|
||||
|
||||
timestamp = result['Data'].get('data', {}).get('timestamp', time.time())
|
||||
docID = "{}".format(self.uuid or '')
|
||||
d_issue = {
|
||||
"device_id": self.device.id,
|
||||
"snapshot": self,
|
||||
"timestamp": timestamp,
|
||||
"issuer_id": g.user.id,
|
||||
"documentId": docID,
|
||||
"key": dpp,
|
||||
}
|
||||
dpp_issue = Dpp(**d_issue)
|
||||
db.session.add(dpp_issue)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '{}. {} version {}.'.format(self.severity, self.software, self.version)
|
||||
|
||||
|
@ -848,7 +992,7 @@ class BenchmarkDataStorage(Benchmark):
|
|||
write_speed = Column(Float(decimal_return_scale=2), nullable=False)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -1576,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):
|
||||
"""The device has been selected for preparation.
|
||||
|
||||
|
@ -1918,7 +2241,8 @@ class Trade(JoinedTableMixin, ActionWithMultipleTradeDocuments):
|
|||
)
|
||||
lot = relationship(
|
||||
'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',
|
||||
)
|
||||
|
||||
|
|
|
@ -442,7 +442,7 @@ class Snapshot(ActionWithOneDevice):
|
|||
required=True,
|
||||
description='The software that generated this Snapshot.',
|
||||
)
|
||||
version = String(required=True, description='The version of the software.')
|
||||
version = Version(required=True, description='The version of the software.')
|
||||
actions = NestedOn(Action, many=True, dump_only=True)
|
||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
|
||||
components = NestedOn(
|
||||
|
@ -469,12 +469,9 @@ class Snapshot(ActionWithOneDevice):
|
|||
|
||||
@validates_schema
|
||||
def validate_components_only_workbench(self, data: dict):
|
||||
software = [
|
||||
SnapshotSoftware.Workbench,
|
||||
SnapshotSoftware.WorkbenchAndroid,
|
||||
SnapshotSoftware.UsodyOS,
|
||||
]
|
||||
if data['software'] not in software:
|
||||
if (data['software'] != SnapshotSoftware.Workbench) and (
|
||||
data['software'] != SnapshotSoftware.WorkbenchAndroid
|
||||
):
|
||||
if data.get('components', None) is not None:
|
||||
raise ValidationError(
|
||||
'Only Workbench can add component info', field_names=['components']
|
||||
|
@ -484,11 +481,7 @@ class Snapshot(ActionWithOneDevice):
|
|||
def validate_only_workbench_fields(self, data: dict):
|
||||
"""Ensures workbench has ``elapsed`` and ``uuid`` and no others."""
|
||||
# todo test
|
||||
software = [
|
||||
SnapshotSoftware.Workbench,
|
||||
SnapshotSoftware.UsodyOS,
|
||||
]
|
||||
if data['software'] in software:
|
||||
if data['software'] == SnapshotSoftware.Workbench:
|
||||
if not data.get('uuid', None):
|
||||
raise ValidationError(
|
||||
'Snapshots from Workbench and WorkbenchAndroid must have uuid',
|
||||
|
@ -530,6 +523,14 @@ class Ready(ActionWithMultipleDevicesCheckingOwner):
|
|||
__doc__ = m.Ready.__doc__
|
||||
|
||||
|
||||
class EWaste(ActionWithMultipleDevicesCheckingOwner):
|
||||
__doc__ = m.EWaste.__doc__
|
||||
|
||||
|
||||
class Recycled(ActionWithMultipleDevicesCheckingOwner):
|
||||
__doc__ = m.Recycled.__doc__
|
||||
|
||||
|
||||
class ActionStatus(Action):
|
||||
rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',))
|
||||
devices = NestedOn(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" This is the view for Snapshots """
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
@ -11,7 +12,7 @@ from flask import g
|
|||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.action.models import Snapshot
|
||||
from ereuse_devicehub.resources.action.models import EraseBasic, Snapshot
|
||||
from ereuse_devicehub.resources.device.models import Computer
|
||||
from ereuse_devicehub.resources.device.sync import Sync
|
||||
from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware
|
||||
|
@ -70,12 +71,9 @@ class SnapshotMixin:
|
|||
snapshot_json = self.snapshot_json
|
||||
device = snapshot_json.pop('device') # type: Computer
|
||||
components = None
|
||||
software = [
|
||||
SnapshotSoftware.Workbench,
|
||||
SnapshotSoftware.WorkbenchAndroid,
|
||||
SnapshotSoftware.UsodyOS,
|
||||
]
|
||||
if snapshot_json['software'] in software:
|
||||
if snapshot_json['software'] == (
|
||||
SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid
|
||||
):
|
||||
components = snapshot_json.pop('components', None)
|
||||
snapshot = Snapshot(**snapshot_json)
|
||||
|
||||
|
@ -124,6 +122,15 @@ class SnapshotMixin:
|
|||
snapshot.device.set_hid()
|
||||
snapshot.device.binding.device.set_hid()
|
||||
|
||||
snapshot.create_json_hw(self.json_wb)
|
||||
snapshot.device.register_dlt()
|
||||
snapshot.register_passport_dlt()
|
||||
|
||||
for ac in snapshot.actions:
|
||||
if not isinstance(ac, EraseBasic):
|
||||
continue
|
||||
ac.register_proof()
|
||||
|
||||
return snapshot
|
||||
|
||||
def is_server_erase(self, snapshot):
|
||||
|
@ -214,6 +221,7 @@ class SnapshotView(SnapshotMixin):
|
|||
self.schema = schema
|
||||
self.resource_def = resource_def
|
||||
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
||||
self.json_wb = copy.copy(snapshot_json)
|
||||
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
|
||||
self.version = snapshot_json.get('version')
|
||||
self.uuid = snapshot_json.get('uuid')
|
||||
|
|
|
@ -13,8 +13,9 @@ from typing import Dict, List, Set
|
|||
|
||||
from boltons import urlutils
|
||||
from citext import CIText
|
||||
from ereuseapi.methods import API
|
||||
from flask import current_app as app
|
||||
from flask import g, request
|
||||
from flask import g, request, session, url_for
|
||||
from more_itertools import unique_everseen
|
||||
from sqlalchemy import BigInteger, Boolean, Column
|
||||
from sqlalchemy import Enum as DBEnum
|
||||
|
@ -353,6 +354,12 @@ class Device(Thing):
|
|||
host_url = request.host_url.strip('/')
|
||||
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
|
||||
def url(self) -> urlutils.URL:
|
||||
"""The URL where to GET this device."""
|
||||
|
@ -476,7 +483,8 @@ class Device(Thing):
|
|||
"""The trading state, or None if no Trade action has
|
||||
ever been performed to this device. This extract the posibilities for to do.
|
||||
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'):
|
||||
return
|
||||
|
||||
|
@ -931,6 +939,85 @@ class Device(Thing):
|
|||
}
|
||||
return types.get(self.type, '')
|
||||
|
||||
def connect_api(self):
|
||||
if 'dpp' not in app.blueprints.keys() or not self.hid:
|
||||
return
|
||||
|
||||
if not session.get('token_dlt'):
|
||||
return
|
||||
|
||||
token_dlt = session.get('token_dlt')
|
||||
api_dlt = app.config.get('API_DLT')
|
||||
if not token_dlt or not api_dlt:
|
||||
return
|
||||
|
||||
return API(api_dlt, token_dlt, "ethereum")
|
||||
|
||||
def register_dlt(self):
|
||||
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
|
||||
|
||||
if result['Status'] == StatusCode.Success.value:
|
||||
timestamp = result.get('Data', {}).get('data', {}).get('timestamp')
|
||||
|
||||
if not timestamp:
|
||||
return
|
||||
|
||||
snapshot = [x for x in self.actions if x.t == 'Snapshot']
|
||||
if not snapshot:
|
||||
return
|
||||
snapshot = snapshot[0]
|
||||
|
||||
d = {
|
||||
"type": PROOF_ENUM['Register'],
|
||||
"device": self,
|
||||
"action": snapshot,
|
||||
"timestamp": timestamp,
|
||||
"issuer_id": g.user.id,
|
||||
"documentId": snapshot.id,
|
||||
"documentSignature": snapshot.phid_dpp,
|
||||
"normalizeDoc": snapshot.json_hw,
|
||||
}
|
||||
proof = Proof(**d)
|
||||
db.session.add(proof)
|
||||
|
||||
if not hasattr(self, 'components'):
|
||||
return
|
||||
|
||||
for c in self.components:
|
||||
if isinstance(c, DataStorage):
|
||||
c.register_dlt()
|
||||
|
||||
def unreliable(self):
|
||||
self.user_trusts = False
|
||||
i = 0
|
||||
|
@ -1246,12 +1333,20 @@ class Placeholder(Thing):
|
|||
return docs.union(self.binding.documents)
|
||||
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):
|
||||
"""A chassis with components inside that can be processed
|
||||
automatically with Workbench Computer.
|
||||
|
||||
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
||||
Computer is broa extended by ``Desktop``, ``Laptop``, and
|
||||
``Server``. The property ``chassis`` defines it more granularly.
|
||||
"""
|
||||
|
||||
|
@ -1747,7 +1842,7 @@ class Processor(JoinedComponentTableMixin, Component):
|
|||
class RamModule(JoinedComponentTableMixin, Component):
|
||||
"""A stick of RAM."""
|
||||
|
||||
size = Column(SmallInteger, check_range('size', min=128, max=70000))
|
||||
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
||||
size.comment = """The capacity of the RAM stick."""
|
||||
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
|
||||
interface = Column(DBEnum(RamInterface))
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
<!-- 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">
|
||||
|
@ -216,5 +217,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import datetime
|
||||
import uuid
|
||||
from itertools import filterfalse
|
||||
|
||||
import flask
|
||||
import marshmallow
|
||||
from ereuseapi.methods import API
|
||||
from flask import Response
|
||||
from flask import current_app as app
|
||||
from flask import g, render_template, request
|
||||
from flask import g, render_template, request, session
|
||||
from flask.json import jsonify
|
||||
from flask_sqlalchemy import Pagination
|
||||
from marshmallow import Schema as MarshmallowSchema
|
||||
from marshmallow import fields
|
||||
from marshmallow import fields as f
|
||||
from marshmallow import validate as v
|
||||
|
@ -24,7 +22,6 @@ from ereuse_devicehub.resources.action.models import Trade
|
|||
from ereuse_devicehub.resources.device import states
|
||||
from ereuse_devicehub.resources.device.models import Computer, Device, Manufacturer
|
||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||
from ereuse_devicehub.resources.enums import SnapshotSoftware
|
||||
from ereuse_devicehub.resources.lot.models import LotDeviceDescendants
|
||||
from ereuse_devicehub.resources.tag.model import Tag
|
||||
from ereuse_devicehub.teal import query
|
||||
|
@ -159,6 +156,7 @@ class DeviceView(View):
|
|||
device_real=device_real,
|
||||
states=states,
|
||||
abstract=abstract,
|
||||
user=g.user,
|
||||
)
|
||||
|
||||
@auth.Auth.requires_auth
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
from typing import Callable, Iterable, Tuple
|
||||
|
||||
from flask import redirect, url_for
|
||||
|
||||
from ereuse_devicehub.teal.resource import Resource, View
|
||||
|
||||
|
||||
class DidView(View):
|
||||
"""
|
||||
This view render one public ans static page for see the links for to do the check
|
||||
of one csv file
|
||||
"""
|
||||
|
||||
def get(self, dpp: str):
|
||||
return redirect(url_for('did.did', id_dpp=dpp))
|
||||
|
||||
|
||||
class DidDef(Resource):
|
||||
__type__ = 'Did'
|
||||
SCHEMA = None
|
||||
VIEW = None # We do not want to create default / documents endpoint
|
||||
AUTH = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
import_name=__name__,
|
||||
static_folder='static',
|
||||
static_url_path=None,
|
||||
template_folder='templates',
|
||||
url_prefix=None,
|
||||
subdomain=None,
|
||||
url_defaults=None,
|
||||
root_path=None,
|
||||
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
|
||||
):
|
||||
super().__init__(
|
||||
app,
|
||||
import_name,
|
||||
static_folder,
|
||||
static_url_path,
|
||||
template_folder,
|
||||
url_prefix,
|
||||
subdomain,
|
||||
url_defaults,
|
||||
root_path,
|
||||
cli_commands,
|
||||
)
|
||||
|
||||
# view = DidView.as_view('main', definition=self, auth=app.auth)
|
||||
|
||||
# if self.AUTH:
|
||||
# view = app.auth.requires_auth(view)
|
||||
|
||||
did_view = DidView.as_view('DidView', definition=self, auth=app.auth)
|
||||
self.add_url_rule(
|
||||
'/<string:dpp>', defaults={}, view_func=did_view, methods={'GET'}
|
||||
)
|
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
|
||||
crossorigin="anonymous">
|
||||
<script src="https://use.fontawesome.com/7553aecc27.js"></script>
|
||||
<title>Devicehub | Stamp create and Stamp verify</title>
|
||||
<style>
|
||||
/*Sticky footer*/
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 6em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default" style="background-color: gainsboro; margin: 0 !important">
|
||||
<div class="container-fluid">
|
||||
<a href="https://www.usody.com/" target="_blank">
|
||||
<h1 align="center">Usody</h1>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="page-header col-md-6 col-md-offset-3">
|
||||
This is the info for chid: {{result.dpp}}
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% for dpp in result.result.data %}
|
||||
<h5>Digital Passport</h5>
|
||||
{{ dpp.dpp }}
|
||||
<h5>Hardware</h5>
|
||||
<ul>
|
||||
<li>Device</li>
|
||||
<ul>
|
||||
<li>Chassis: {{ dpp.hardware.device.chassis }}</li>
|
||||
<li>Manufacturer: {{ dpp.hardware.device.manufacturer }}</li>
|
||||
<li>Model: {{ dpp.hardware.device.model }}</li>
|
||||
<li>SerialNumber: {{ dpp.hardware.device.serialNumber }}</li>
|
||||
<li>Sku: {{ dpp.hardware.device.sku }}</li>
|
||||
<li>Type: {{ dpp.hardware.device.type }}</li>
|
||||
<li>Version: {{ dpp.hardware.device.version }}</li>
|
||||
</ul>
|
||||
<li>Components</li>
|
||||
<ul>
|
||||
{% for component in dpp.hardware.components %}
|
||||
<li>{{ component }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</ul>
|
||||
<hr />
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,140 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
|
||||
<title>Device {{ result.result.data.hardware.device.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>
|
||||
|
||||
<!-- 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">
|
||||
<div class="col-xl-12">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="nav-link mt-5" style="color: #993365">
|
||||
{{ result.result.data.hardware.device.type }} -
|
||||
{{ result.result.data.hardware.device.manufacturer }}
|
||||
{{ result.result.data.hardware.device.model }}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
This is the info for Digital Passport:<br />
|
||||
{{ result.result.data.dpp }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="card-title">Device</h5>
|
||||
{% for key, value in result.result.data.hardware.device.items() %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ key }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ value or '' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="card-title">Components</h5>
|
||||
{% for component in result.result.data.hardware.components %}
|
||||
{% for key, value in component.items() %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ key }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ value or '' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if result.result.data.url_last %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="card-title">Last Dpp</h5>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a href="{{ result.result.data.url_last }}">Last Dpp</a>
|
||||
</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">
|
||||
© 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>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,26 @@
|
|||
{% import 'devices/macros.html' as macros %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
|
||||
crossorigin="anonymous">
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ url_for('Document.static', filename='print.css') }}">
|
||||
<title>USOdy | {{ title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<header class="page-header">
|
||||
<h1> {{ title }}</h1>
|
||||
</header>
|
||||
</div>
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootswatch/3.3.7/flatly/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-+ENW/yibaokMnme+vBLnHMphUYxHs34h9lpdbSLuAwGkOKFRl4C34WkjazBtb7eT"
|
||||
crossorigin="anonymous">
|
||||
<script src="https://use.fontawesome.com/7553aecc27.js"></script>
|
||||
<title>Devicehub | Stamp create and Stamp verify</title>
|
||||
<style>
|
||||
/*Sticky footer*/
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 6em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default" style="background-color: gainsboro; margin: 0 !important">
|
||||
<div class="container-fluid">
|
||||
<a href="https://www.usody.com/" target="_blank">
|
||||
<h1 align="center">Usody</h1>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container-fluid">
|
||||
<div class="page-header col-md-6 col-md-offset-3">
|
||||
<div class="row">
|
||||
{% if result.0 == 'Ok' %}
|
||||
<div class="alert alert-info" style="background-color: #3fb618;" role="alert">
|
||||
{{ result.1 }}
|
||||
</div>
|
||||
{% elif result.0 == 'Bad' %}
|
||||
<div class="alert alert-danger" style="background-color: #ff0039" role="alert">
|
||||
{{ result.1 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="http://dlt.ereuse.org/stamps/create?url={{ rq_url }}" target="_blank">Add one new check in your csv</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="http://dlt.ereuse.org/stamps/check?url={{ rq_url }}" target="_blank">Verify a CSV file in here.</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="page-header col-md-6 col-md-offset-3">
|
||||
If you want us to verify a document issued by us, upload it using the following form
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form enctype="multipart/form-data" action="" method="post">
|
||||
<label for="name">Document: </label>
|
||||
<input type="file" name="docUpload" accept="*" /><br />
|
||||
<input type="submit" id="send-signup" name="signup" value="Verify" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -15,7 +15,6 @@ class SnapshotSoftware(Enum):
|
|||
Web = 'Web'
|
||||
DesktopApp = 'DesktopApp'
|
||||
WorkbenchDesktop = 'WorkbenchDesktop'
|
||||
UsodyOS = 'UsodyOS'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -367,7 +366,7 @@ class ErasureStandards(Enum):
|
|||
|
||||
HMG_IS5 = 'British HMG Infosec Standard 5 (HMG IS5)'
|
||||
"""`British HMG Infosec Standard 5 (HMG IS5)
|
||||
<https://en.wikipedia.org/wiki/Infosec_Standard_5>`_.
|
||||
<https://en.wikipedia.org/wiki/Infosec_Standard_5>`.
|
||||
|
||||
In order to follow this standard, an erasure must have the
|
||||
following steps:
|
||||
|
@ -379,14 +378,6 @@ class ErasureStandards(Enum):
|
|||
And be an :class:`ereuse_devicehub.resources.action.models.EraseSectors`.
|
||||
"""
|
||||
|
||||
NIST = "Infosec HGM Baseline"
|
||||
"""Method for securely erasing data in compliance with HMG Infosec Standard 5
|
||||
guidelines includes a single step of a random write process on the full disk.
|
||||
This process overwrites all data with a randomized pattern, ensuring that
|
||||
it cannot be recovered. Built-in validation confirms that the data has been
|
||||
written correctly, and a final validation confirms that all data has been deleted.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
@ -454,3 +445,13 @@ class SessionType(IntEnum):
|
|||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class StatusCode(IntEnum):
|
||||
"""The code of the status response of api dlt."""
|
||||
|
||||
Success = 201
|
||||
NotWork = 400
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -51,6 +51,7 @@ class Lot(Thing):
|
|||
primaryjoin=lambda: Lot.id == LotParent.child_id,
|
||||
secondaryjoin=lambda: LotParent.parent_id == Lot.id,
|
||||
cascade='refresh-expire', # propagate changes outside ORM
|
||||
sync_backref=False,
|
||||
backref=db.backref(
|
||||
'children',
|
||||
viewonly=True,
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import json
|
||||
import requests
|
||||
from uuid import uuid4
|
||||
|
||||
from citext import CIText
|
||||
from ereuseapi.methods import API
|
||||
from flask import current_app as app
|
||||
from flask import g
|
||||
from flask import g, session
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import BigInteger, Boolean, Column, Sequence
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
@ -29,6 +33,8 @@ class User(UserMixin, Thing):
|
|||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||
active = Column(Boolean, default=True, nullable=False)
|
||||
phantom = Column(Boolean, default=False, nullable=False)
|
||||
api_keys_dlt = Column(CIText(), nullable=True)
|
||||
rols_dlt = Column(CIText(), nullable=True)
|
||||
inventories = db.relationship(
|
||||
Inventory,
|
||||
backref=db.backref('users', lazy=True, collection_class=set),
|
||||
|
@ -38,6 +44,9 @@ class User(UserMixin, Thing):
|
|||
|
||||
# todo set restriction that user has, at least, one active db
|
||||
|
||||
def get_user_id(self):
|
||||
return self.id
|
||||
|
||||
def __init__(
|
||||
self, email, password=None, inventories=None, active=True, phantom=False
|
||||
) -> None:
|
||||
|
@ -94,6 +103,150 @@ class User(UserMixin, Thing):
|
|||
# take advantage of SQL Alchemy PasswordType to verify password
|
||||
return self.password == password
|
||||
|
||||
def set_new_dlt_keys(self, password):
|
||||
if 'dpp' not in app.blueprints.keys():
|
||||
return
|
||||
|
||||
from ereuseapi.methods import register_user
|
||||
|
||||
from ereuse_devicehub.modules.dpp.utils import encrypt
|
||||
|
||||
api_dlt = app.config.get('API_DLT')
|
||||
data = register_user(api_dlt)
|
||||
api_token = data.get('data', {}).get('api_token')
|
||||
data = json.dumps(data)
|
||||
self.api_keys_dlt = encrypt(password, data)
|
||||
return api_token
|
||||
|
||||
def get_dlt_keys(self, password):
|
||||
if 'dpp' not in app.blueprints.keys():
|
||||
return {}
|
||||
|
||||
from ereuse_devicehub.modules.dpp.utils import decrypt
|
||||
|
||||
if not self.api_keys_dlt:
|
||||
return {}
|
||||
|
||||
data = decrypt(password, self.api_keys_dlt)
|
||||
return json.loads(data)
|
||||
|
||||
def reset_dlt_keys(self, password, data):
|
||||
if 'dpp' not in app.blueprints.keys():
|
||||
return
|
||||
|
||||
from ereuse_devicehub.modules.dpp.utils import encrypt
|
||||
|
||||
data = json.dumps(data)
|
||||
self.api_keys_dlt = encrypt(password, data)
|
||||
|
||||
def allow_permitions(self, api_token=None, rols="Operator"):
|
||||
# Is discontinued over IOTA branch
|
||||
return
|
||||
|
||||
# if 'dpp' not in app.blueprints.keys():
|
||||
# return
|
||||
|
||||
# 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
|
||||
|
||||
# apiUser1 = API(api_dlt, keyUser1, "ethereum")
|
||||
|
||||
# for rol in rols.split(","):
|
||||
# result = apiUser1.issue_credential(rol.strip(), target_user)
|
||||
# return result
|
||||
|
||||
def get_rols_dlt(self):
|
||||
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):
|
||||
|
||||
if 'dpp' not in app.blueprints.keys():
|
||||
return []
|
||||
|
||||
if not token_dlt:
|
||||
token_dlt = session.get('token_dlt')
|
||||
if not token_dlt:
|
||||
return []
|
||||
|
||||
api_dlt = app.config.get('API_DLT')
|
||||
if not api_dlt:
|
||||
return []
|
||||
|
||||
self.get_abac_did()
|
||||
role = session.get('iota_abac_attributes', {}).get('role', '')
|
||||
return [(x.strip(), x.strip()) for x in role.split(",")]
|
||||
|
||||
def _call_abac(self, path):
|
||||
abac_tk = app.config.get('ABAC_TOKEN')
|
||||
# abac_coockie = app.config.get('ABAC_COOKIE')
|
||||
domain = app.config.get('ABAC_URL')
|
||||
eth_pub_key = session.get('eth_pub_key')
|
||||
|
||||
abac_path = path
|
||||
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 {}
|
||||
|
||||
|
||||
|
||||
class UserInventory(db.Model):
|
||||
"""Relationship between users and their inventories."""
|
||||
|
|
|
@ -240,9 +240,10 @@ class SQLAlchemy(FlaskSQLAlchemy):
|
|||
metadata=None,
|
||||
query_class=BaseQuery,
|
||||
model_class=Model,
|
||||
engine_options=None,
|
||||
):
|
||||
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):
|
||||
|
@ -266,9 +267,10 @@ class SchemaSQLAlchemy(SQLAlchemy):
|
|||
metadata=None,
|
||||
query_class=Query,
|
||||
model_class=Model,
|
||||
engine_options=None,
|
||||
):
|
||||
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
|
||||
# schema and create the schemas accordingly
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import ipaddress
|
||||
from distutils.version import LooseVersion
|
||||
from distutils.version import StrictVersion
|
||||
from typing import Type, Union
|
||||
|
||||
import colour
|
||||
from boltons import strutils, urlutils
|
||||
from ereuse_devicehub.ereuse_utils import if_none_return_none
|
||||
from flask import current_app as app
|
||||
from flask import g
|
||||
from marshmallow import utils
|
||||
|
@ -16,7 +17,6 @@ from marshmallow.validate import Validator
|
|||
from marshmallow_enum import EnumField as _EnumField
|
||||
from sqlalchemy_utils import PhoneNumber
|
||||
|
||||
from ereuse_devicehub.ereuse_utils import if_none_return_none
|
||||
from ereuse_devicehub.teal import db as tealdb
|
||||
from ereuse_devicehub.teal.resource import Schema
|
||||
|
||||
|
@ -30,7 +30,7 @@ class Version(Field):
|
|||
|
||||
@if_none_return_none
|
||||
def _deserialize(self, value, attr, data):
|
||||
return LooseVersion(value)
|
||||
return StrictVersion(value)
|
||||
|
||||
|
||||
class Color(Field):
|
||||
|
|
|
@ -39,6 +39,14 @@
|
|||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-sanitization-entity">Sanitization Certificate</button>
|
||||
</li>
|
||||
{% if oidc %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('oidc.create_client') }}" class="nav-link">OpenID Connect</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#id_abac_attrs">Identity Attributes</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content pt-2">
|
||||
|
||||
|
@ -98,6 +106,29 @@
|
|||
</form><!-- End Sanitization Certificate datas Form -->
|
||||
</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>
|
||||
|
|
|
@ -85,6 +85,15 @@
|
|||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button>
|
||||
</li>
|
||||
|
||||
{% if placeholder.binding %}
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#dpps">Digital Passports</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#proofs">Proofs</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
<div class="tab-content pt-2">
|
||||
|
||||
|
@ -354,6 +363,79 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if placeholder.binding %}
|
||||
<div class="tab-pane fade profile-overview" id="dpps">
|
||||
<h5 class="card-title">Digital Passports</h5>
|
||||
{% for dpp in placeholder.binding.dpps %}
|
||||
<div class="list-group col-12">
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">Time Stamp</h5>
|
||||
<small class="text-muted">{{ dpp.timestamp }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="mb-1">
|
||||
<a href="{{ url_for('Did.DidView', dpp=dpp.key) }}" target="_blank">{{ dpp.key }}</a><br />
|
||||
</p>
|
||||
<small class="text-muted">{{ dpp.created.strftime('%H:%M %d-%m-%Y') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade profile-overview" id="proofs">
|
||||
<h5 class="card-title">Proofs</h5>
|
||||
{% for proof in placeholder.proofs %}
|
||||
<div class="list-group col-12">
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">Time Stamp</h5>
|
||||
<small class="text-muted">{{ proof.timestamp }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="mb-1">
|
||||
{{ proof.type }}<br />
|
||||
</p>
|
||||
<small class="text-muted">{{ proof.created.strftime('%H:%M %d-%m-%Y') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for component in placeholder.binding.components %}
|
||||
{% for proof in component.proofs %}
|
||||
<div class="list-group col-12">
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">Time Stamp</h5>
|
||||
<small class="text-muted">{{ proof.timestamp }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="mb-1">
|
||||
{{ proof.type }}<br />
|
||||
</p>
|
||||
<small class="text-muted">{{ proof.created.strftime('%H:%M %d-%m-%Y') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -226,6 +226,18 @@
|
|||
Ready
|
||||
</a>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -192,7 +192,7 @@
|
|||
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
{% for dev in devices %}
|
||||
qr_draw("{{ dev.public_link }}", "#{{ dev.dhid }}")
|
||||
qr_draw("{{ dev.chid_link }}", "#{{ dev.dhid }}")
|
||||
{% endfor %}
|
||||
</script>
|
||||
{% endblock main %}
|
||||
|
|
|
@ -2,7 +2,7 @@ import flask
|
|||
from decouple import config
|
||||
from flask import Blueprint
|
||||
from flask import current_app as app
|
||||
from flask import g
|
||||
from flask import g, session
|
||||
from flask.views import View
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
from sqlalchemy import or_
|
||||
|
@ -28,6 +28,10 @@ class LoginView(View):
|
|||
template_name = 'ereuse_devicehub/user_login.html'
|
||||
|
||||
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()
|
||||
if form.validate_on_submit():
|
||||
# Login and validate the user.
|
||||
|
@ -64,8 +68,18 @@ class LoginView(View):
|
|||
|
||||
class LogoutView(View):
|
||||
def dispatch_request(self):
|
||||
session_vars = [
|
||||
'token_dlt',
|
||||
'eth_pub_key',
|
||||
'rols',
|
||||
'oidc',
|
||||
'iota_abac_did',
|
||||
'iota_abac_attributes',
|
||||
]
|
||||
[session.pop(i, '') for i in session_vars]
|
||||
next_url = flask.request.args.get('next')
|
||||
logout_user()
|
||||
return flask.redirect(flask.url_for('core.login'))
|
||||
return flask.redirect(next_url or flask.url_for('core.login'))
|
||||
|
||||
|
||||
class GenericMixin(View):
|
||||
|
@ -105,11 +119,13 @@ class UserProfileView(GenericMixin):
|
|||
if g.user.sanitization_entity:
|
||||
sanitization = g.user.sanitization_entity
|
||||
sanitization_form = SanitizationEntityForm(obj=sanitization)
|
||||
oidc = 'oidc' in app.blueprints.keys()
|
||||
self.context.update(
|
||||
{
|
||||
'current_user': current_user,
|
||||
'password_form': PasswordForm(),
|
||||
'sanitization_form': sanitization_form,
|
||||
'oidc': oidc,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -11,32 +11,12 @@ from ereuse_devicehub.config import DevicehubConfig
|
|||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.inventory.views import devices
|
||||
from ereuse_devicehub.labels.views import labels
|
||||
from ereuse_devicehub.mail.flask_mail import Mail
|
||||
from ereuse_devicehub.views import core
|
||||
from ereuse_devicehub.workbench.views import workbench
|
||||
|
||||
# from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
|
||||
# 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,
|
||||
)
|
||||
|
||||
from ereuse_devicehub.modules.did.views import did
|
||||
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
|
||||
|
||||
app = Devicehub(inventory=DevicehubConfig.DB_SCHEMA)
|
||||
app.register_blueprint(core)
|
||||
|
@ -44,17 +24,10 @@ app.register_blueprint(devices)
|
|||
app.register_blueprint(labels)
|
||||
app.register_blueprint(api)
|
||||
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
|
||||
# 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)
|
||||
config_oauth(app)
|
||||
|
||||
|
|
|
@ -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_PASSWORD='ereuse'
|
||||
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'
|
||||
|
|
|
@ -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 .
|
|
@ -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"}
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"email": "user@example.org",
|
||||
"password": "1234",
|
||||
"eth_pub_key": "0x0000000000000000000000000000000000000000",
|
||||
"eth_priv_key":"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
set -u
|
||||
# DEBUG
|
||||
set -x
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")"
|
||||
|
||||
make docker_build
|
||||
docker compose up
|
||||
}
|
||||
|
||||
main "${@}"
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue