Compare commits
85 Commits
disconnect
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
466cb344e6 | ||
| 3ae6db1184 | |||
| b18f9ba616 | |||
|
|
c8a517ff60 | ||
|
|
9c19708381 | ||
| 160076afcf | |||
|
|
437cec38e0 | ||
|
|
82b3e78488 | ||
|
|
16e4231807 | ||
| dc142bca0c | |||
|
|
cc334f1727 | ||
| de398d189a | |||
|
|
1d4c4c8ec2 | ||
|
|
6b50334741 | ||
|
|
bd8e60bf5d | ||
|
|
26251ed8e7 | ||
| f181e4a785 | |||
| 1d011af64b | |||
| 6ba38cd42d | |||
| 1745f56ac7 | |||
|
|
6971b4b618 | ||
| bb4db25dcd | |||
|
|
da200f95b8 | ||
|
|
190f728eb7 | ||
| a709dbcaef | |||
| c4b54357f7 | |||
|
|
7700b4381f | ||
|
|
60ff5bdeaf | ||
| bc1347fe99 | |||
| 9ef9d98132 | |||
| bab6403f91 | |||
| 692649e08f | |||
| 6f1e01781a | |||
| 7224038e64 | |||
| c96cdfc6e1 | |||
| c3d8a1e91a | |||
| b214d161bf | |||
| e19ce060d8 | |||
|
|
6081486a35 | ||
|
|
2da856dd36 | ||
|
|
872e531ef5 | ||
|
|
4563e16137 | ||
|
|
8b05bb694f | ||
| 5d731c9fba | |||
|
|
36c889956f | ||
|
|
a408fd3b4c | ||
|
|
32c2674210 | ||
| d61c395d2c | |||
| 6b4f7c8118 | |||
| 310d5f4820 | |||
| 9ab0d43f43 | |||
|
|
859a5d880a | ||
|
|
f70cbdc463 | ||
| 48505b76ea | |||
| c0e56cf40d | |||
|
|
123eb65f8e | ||
|
|
186614bc4a | ||
|
|
51536766bf | ||
|
|
3c6b6ba099 | ||
| 4d01f7025b | |||
|
|
3a7d8dbdd4 | ||
|
|
88f5c3023d | ||
|
|
4485dea583 | ||
| 70df74cecf | |||
|
|
fad2238b75 | ||
|
|
295aadfaeb | ||
| 07d01304be | |||
|
|
254637bfa9 | ||
| 3ecc8e0955 | |||
| cbc269edf2 | |||
|
|
2451a56403 | ||
|
|
f0783d97c8 | ||
| 8b7b2f489c | |||
| cfa12717e0 | |||
|
|
8cf8632c8c | ||
| 024f063bea | |||
|
|
ed57dc2720 | ||
|
|
cad27733f0 | ||
|
|
bc50e23a22 | ||
|
|
ebc34e396d | ||
| 0469b8dbb5 | |||
|
|
bef3c6dc9b | ||
|
|
9d9c58d268 | ||
| bd9b6c61d3 | |||
| 1c8cb55b46 |
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
backend/models/initial_recorders.json filter=git-crypt diff=git-crypt
|
||||||
|
#backend/models/initial_recorders.json filter=openssl diff=openssl
|
||||||
|
backend/config.py filter=git-crypt diff=git-crypt
|
||||||
|
#backend/config.py filter=openssl diff=openssl
|
||||||
|
[merge]
|
||||||
|
renormalize=true
|
||||||
120
.gitignore
vendored
120
.gitignore
vendored
@@ -1 +1,121 @@
|
|||||||
|
app.db
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
.idea
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
backend/uploads/*
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
logs
|
||||||
|
|||||||
33
Pipfile
33
Pipfile
@@ -4,15 +4,44 @@ verify_ssl = true
|
|||||||
name = "pypi"
|
name = "pypi"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
|
ffmpeg-python = "*"
|
||||||
flask = "*"
|
flask = "*"
|
||||||
flask-httpauth = "*"
|
flask-httpauth = "*"
|
||||||
flask-restplus-patched = "*"
|
|
||||||
flask-sqlalchemy = "*"
|
flask-sqlalchemy = "*"
|
||||||
flask-login = "*"
|
flask-login = "*"
|
||||||
pyjwt = "*"
|
pyjwt = "*"
|
||||||
passlib = "*"
|
passlib = "*"
|
||||||
|
sqlalchemy = "*"
|
||||||
|
sqlalchemy-migrate = "*"
|
||||||
|
flask-script = "*"
|
||||||
|
flask-migrate = "*"
|
||||||
|
coverage = "*"
|
||||||
|
flask-testing = "*"
|
||||||
|
flask-pyoidc = "*"
|
||||||
|
python-jose = "*"
|
||||||
|
flask-jwt-extended = "*"
|
||||||
|
ssh2-python = "*"
|
||||||
|
update = "*"
|
||||||
|
flask-cors = "*"
|
||||||
|
html5lib = "*"
|
||||||
|
beautifulsoup4 = "*"
|
||||||
|
flask-socketio = "*"
|
||||||
|
eventlet = "*"
|
||||||
|
ics = "*"
|
||||||
|
coloredlogs = "*"
|
||||||
|
pythonping = "*"
|
||||||
|
scapy = "*"
|
||||||
|
python-socketio = {version = "*",extras = ["client"]}
|
||||||
|
socketio-client = "*"
|
||||||
|
websocket-client = "*"
|
||||||
|
apscheduler = "*"
|
||||||
|
pillow = "*"
|
||||||
|
pydub = "*"
|
||||||
|
simpleaudio = "*"
|
||||||
|
flask-restx = "*"
|
||||||
|
cac = {git = "https://git.scc.kit.edu/sg8018/campus_api_client"}
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.7"
|
python_version = "3.8"
|
||||||
|
|||||||
835
Pipfile.lock
generated
835
Pipfile.lock
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "196d1a010314c8f16ccc747ed35b4821e8e3aa63f90ec2893123ea19c95935b1"
|
"sha256": "12e20b3ff7868a6e8c6bd6781acb0deaed105ba25bc22ede80660c47f07c940b"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
"python_version": "3.7"
|
"python_version": "3.8"
|
||||||
},
|
},
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
@@ -16,106 +16,429 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
|
"alembic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.4.2"
|
||||||
|
},
|
||||||
"aniso8601": {
|
"aniso8601": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:29ad6be3828ab6ac2a31fd2876fd84477cde11890ffca7e8a9434aad5d4acec8",
|
"sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072",
|
||||||
"sha256:a5c7595bb65d3919a9944a759d907b57c4d050abaa0e5cf845e84c26cdfd1218"
|
"sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a"
|
||||||
],
|
],
|
||||||
"version": "==5.1.0"
|
"version": "==8.0.0"
|
||||||
},
|
},
|
||||||
"apispec": {
|
"apscheduler": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:57a7b81fd19fff0663a7e5ffd196eaea79b5364151ed2b65533be36d55e0229c",
|
"sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244",
|
||||||
"sha256:b45def53903516e67e8584ee41f34bc60c3e4acace6892b69340293ea20f3caa"
|
"sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"
|
||||||
],
|
],
|
||||||
"version": "==1.0.0"
|
"index": "pypi",
|
||||||
|
"version": "==3.6.3"
|
||||||
|
},
|
||||||
|
"arrow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4bfacea734ead51495dc47df00421ecfd4ca1f2c0fbe58b9a26eaeddedc31caf",
|
||||||
|
"sha256:67f8be7c0cf420424bc62d8d7dc40b44e4bb2f7b515f9cc2954fb36e35797656"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==0.14.7"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||||
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||||
],
|
],
|
||||||
"version": "==19.1.0"
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==19.3.0"
|
||||||
|
},
|
||||||
|
"beaker": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ad5d1c05027ee3be3a482ea39f8cb70339b41e5d6ace0cb861382754076d187e"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"beautifulsoup4": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:73cc4d115b96f79c7d77c1c7f7a0a8d4c57860d1041df407dd1aae7f07a77fd7",
|
||||||
|
"sha256:a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8",
|
||||||
|
"sha256:e718f2342e2e099b640a34ab782407b7b676f47ee272d6739e60b8ea23829f2c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.9.1"
|
||||||
|
},
|
||||||
|
"cac": {
|
||||||
|
"git": "https://git.scc.kit.edu/sg8018/campus_api_client",
|
||||||
|
"ref": "735b763284408c34be7803315829dc54ab9472a8"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||||
|
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||||
|
],
|
||||||
|
"version": "==2020.6.20"
|
||||||
|
},
|
||||||
|
"cffi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
|
||||||
|
"sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
|
||||||
|
"sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
|
||||||
|
"sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
|
||||||
|
"sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
|
||||||
|
"sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
|
||||||
|
"sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
|
||||||
|
"sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
|
||||||
|
"sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
|
||||||
|
"sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
|
||||||
|
"sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
|
||||||
|
"sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
|
||||||
|
"sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
|
||||||
|
"sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
|
||||||
|
"sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
|
||||||
|
"sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
|
||||||
|
"sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
|
||||||
|
"sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
|
||||||
|
"sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
|
||||||
|
"sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
|
||||||
|
"sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
|
||||||
|
"sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
|
||||||
|
"sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
|
||||||
|
"sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
|
||||||
|
"sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
|
||||||
|
"sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
|
||||||
|
"sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
|
||||||
|
"sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
|
||||||
|
],
|
||||||
|
"version": "==1.14.1"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||||
],
|
],
|
||||||
"version": "==7.0"
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==7.1.2"
|
||||||
|
},
|
||||||
|
"coloredlogs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:346f58aad6afd48444c2468618623638dadab76e4e70d5e10822676f2d32226a",
|
||||||
|
"sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==14.0"
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
|
||||||
|
"sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
|
||||||
|
"sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
|
||||||
|
"sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
|
||||||
|
"sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
|
||||||
|
"sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
|
||||||
|
"sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
|
||||||
|
"sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
|
||||||
|
"sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
|
||||||
|
"sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
|
||||||
|
"sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
|
||||||
|
"sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
|
||||||
|
"sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
|
||||||
|
"sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
|
||||||
|
"sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
|
||||||
|
"sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
|
||||||
|
"sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
|
||||||
|
"sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
|
||||||
|
"sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
|
||||||
|
"sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
|
||||||
|
"sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
|
||||||
|
"sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
|
||||||
|
"sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
|
||||||
|
"sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
|
||||||
|
"sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
|
||||||
|
"sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
|
||||||
|
"sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
|
||||||
|
"sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
|
||||||
|
"sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
|
||||||
|
"sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
|
||||||
|
"sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
|
||||||
|
"sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
|
||||||
|
"sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
|
||||||
|
"sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.2.1"
|
||||||
|
},
|
||||||
|
"cryptography": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b",
|
||||||
|
"sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd",
|
||||||
|
"sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a",
|
||||||
|
"sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07",
|
||||||
|
"sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71",
|
||||||
|
"sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756",
|
||||||
|
"sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559",
|
||||||
|
"sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f",
|
||||||
|
"sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261",
|
||||||
|
"sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053",
|
||||||
|
"sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2",
|
||||||
|
"sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f",
|
||||||
|
"sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b",
|
||||||
|
"sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77",
|
||||||
|
"sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83",
|
||||||
|
"sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f",
|
||||||
|
"sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67",
|
||||||
|
"sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c",
|
||||||
|
"sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==3.0"
|
||||||
|
},
|
||||||
|
"decorator": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
|
||||||
|
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
|
||||||
|
],
|
||||||
|
"version": "==4.4.2"
|
||||||
|
},
|
||||||
|
"defusedxml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
|
||||||
|
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"dnspython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01",
|
||||||
|
"sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.16.0"
|
||||||
|
},
|
||||||
|
"ecdsa": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:64c613005f13efec6541bb0a33290d0d03c27abab5f15fbab20fb0ee162bdd8e",
|
||||||
|
"sha256:e108a5fe92c67639abae3260e43561af914e7fd0d27bae6d2ec1312ae7934dfe"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.14.1"
|
||||||
|
},
|
||||||
|
"eventlet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4f4a43366b4cbd4a3f2f231816e5c3dae8ab316df9b7da11f0525e2800559f33",
|
||||||
|
"sha256:faa384fdd8e5797ab1f16979576fc93ce89a7e1e5622d345caec1bf767a274fd"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.26.1"
|
||||||
|
},
|
||||||
|
"ffmpeg-python": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127",
|
||||||
|
"sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.0"
|
||||||
},
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
|
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
|
||||||
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
|
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.0.2"
|
"version": "==1.1.2"
|
||||||
|
},
|
||||||
|
"flask-cors": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
|
||||||
|
"sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.8"
|
||||||
},
|
},
|
||||||
"flask-httpauth": {
|
"flask-httpauth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c08b69b302f1aa7ecd0db327809132ef6ca9486a36a9174776da146d1a4adc18",
|
"sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7",
|
||||||
"sha256:f71b7611f385fbdf350e8c430eed17b41c3b2200dc35eae19c1734264b68e31d"
|
"sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.4"
|
"version": "==4.1.0"
|
||||||
|
},
|
||||||
|
"flask-jwt-extended": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.24.1"
|
||||||
},
|
},
|
||||||
"flask-login": {
|
"flask-login": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"
|
"sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
|
||||||
|
"sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.4.1"
|
"version": "==0.5.0"
|
||||||
},
|
},
|
||||||
"flask-marshmallow": {
|
"flask-migrate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:75c9d80f22af982b1e8ccec109d3b75c14bb5570602ae3705a4ff775badd2816",
|
"sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732",
|
||||||
"sha256:db7aff4130eb99fd05ab78fd2e2c58843ba0f208899aeb1c14aff9cd98ae8c80"
|
"sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee"
|
||||||
],
|
|
||||||
"version": "==0.9.0"
|
|
||||||
},
|
|
||||||
"flask-restplus": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3fad697e1d91dfc13c078abcb86003f438a751c5a4ff41b84c9050199d2eab62",
|
|
||||||
"sha256:cdc27b5be63f12968a7f762eaa355e68228b0c904b4c96040a314ba7dc6d0e69"
|
|
||||||
],
|
|
||||||
"version": "==0.12.1"
|
|
||||||
},
|
|
||||||
"flask-restplus-patched": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:36342775f9e0990dfc000dbe61133dfe56f9ef32c9b4c6293ba7f2c128d16efc"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.1.10"
|
"version": "==2.5.3"
|
||||||
|
},
|
||||||
|
"flask-pyoidc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6e8381fbb64ca62a37522e69c67e04f9074c54a7b81dcb7431fd290d79c4111b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.5.0"
|
||||||
|
},
|
||||||
|
"flask-restx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a1653da19ca0b5e5c2ea59bd5f4639a7749e6a9b882f459de1814ed37872253b",
|
||||||
|
"sha256:ca87a1808333f4ec5a50a5740b44e6cd3879a4b940d559df3996877ec4a2f2a5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"flask-script": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6425963d91054cfcc185807141c7314a9c5ad46325911bd24dcb489bd0161c65"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0.6"
|
||||||
|
},
|
||||||
|
"flask-socketio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3668675bf7763c5b5f56689d439f07356e89c0a52e0c9e9cd3cc08563c07b252",
|
||||||
|
"sha256:36c1d5765010d1f4e4f05b4cc9c20c289d9dc70698c88d1addd0afcfedc5b062"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.3.1"
|
||||||
},
|
},
|
||||||
"flask-sqlalchemy": {
|
"flask-sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",
|
"sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
|
||||||
"sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"
|
"sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.3.2"
|
"version": "==2.4.4"
|
||||||
|
},
|
||||||
|
"flask-testing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:d849bf53eb1ceb09dff6611888588cb60f20238058fb1ebcd917d69febc373e6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.8.0"
|
||||||
|
},
|
||||||
|
"future": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.18.2"
|
||||||
|
},
|
||||||
|
"greenlet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52",
|
||||||
|
"sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b",
|
||||||
|
"sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384",
|
||||||
|
"sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691",
|
||||||
|
"sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5",
|
||||||
|
"sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e",
|
||||||
|
"sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1",
|
||||||
|
"sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c",
|
||||||
|
"sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873",
|
||||||
|
"sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872",
|
||||||
|
"sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727",
|
||||||
|
"sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9",
|
||||||
|
"sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721",
|
||||||
|
"sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7",
|
||||||
|
"sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0",
|
||||||
|
"sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92",
|
||||||
|
"sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"
|
||||||
|
],
|
||||||
|
"version": "==0.4.16"
|
||||||
|
},
|
||||||
|
"html5lib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
|
||||||
|
"sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.1"
|
||||||
|
},
|
||||||
|
"humanfriendly": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:bf52ec91244819c780341a3438d5d7b09f431d3f113a475147ac9b7b167a3d12",
|
||||||
|
"sha256:e78960b31198511f45fd455534ae7645a6207d33e512d2e842c766d15d9c8080"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==8.2"
|
||||||
|
},
|
||||||
|
"ics": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3b606205b9582ad27dff77f9b227a30d02fdac532731927fe39df1f1ddf8673f",
|
||||||
|
"sha256:bf5fbdef6e1e073afdadf1b996f0271186dd114a148e38e795919a1ae644d6ac"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||||
|
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"importlib-resources": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3",
|
||||||
|
"sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==3.0.0"
|
||||||
},
|
},
|
||||||
"itsdangerous": {
|
"itsdangerous": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.1.0"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"jinja2": {
|
"jinja2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||||
],
|
],
|
||||||
"version": "==2.10"
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==2.11.2"
|
||||||
},
|
},
|
||||||
"jsonschema": {
|
"jsonschema": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d",
|
"sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163",
|
||||||
"sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"
|
"sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"
|
||||||
],
|
],
|
||||||
"version": "==3.0.1"
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"mako": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27",
|
||||||
|
"sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.1.3"
|
||||||
},
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -123,13 +446,16 @@
|
|||||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||||
|
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||||
|
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||||
|
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||||
@@ -146,24 +472,150 @@
|
|||||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||||
|
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||||
|
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"marshmallow": {
|
"monotonic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6eeaf1301a5f5942bfe8ab2c2eaf03feb793072b56d5fae563638bddd7bb62e6",
|
"sha256:23953d55076df038541e648a53676fb24980f7a1be290cdda21300b3bc21dfb0",
|
||||||
"sha256:f72a206432a3369dd72824564d18d915761e07805c05f00d0dcc7885fac1e385"
|
"sha256:552a91f381532e33cbd07c6a2655a21908088962bb8fa7239ecbcc6ad1140cc7"
|
||||||
],
|
],
|
||||||
"version": "==2.18.1"
|
"version": "==1.5"
|
||||||
|
},
|
||||||
|
"oic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:091b20c0a4866e5afeef8fc21bfdffd65382763f09d782e14f8ce9081326e1ed",
|
||||||
|
"sha256:865da7ade1291c2f39dd196c34e5641a782b29871c3a48289e317d62fa49ef20"
|
||||||
|
],
|
||||||
|
"markers": "python_version ~= '3.5'",
|
||||||
|
"version": "==1.1.2"
|
||||||
},
|
},
|
||||||
"passlib": {
|
"passlib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
|
"sha256:68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177",
|
||||||
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
|
"sha256:8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.7.1"
|
"version": "==1.7.2"
|
||||||
|
},
|
||||||
|
"pbr": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c",
|
||||||
|
"sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"
|
||||||
|
],
|
||||||
|
"version": "==5.4.5"
|
||||||
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f",
|
||||||
|
"sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8",
|
||||||
|
"sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad",
|
||||||
|
"sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f",
|
||||||
|
"sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae",
|
||||||
|
"sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d",
|
||||||
|
"sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5",
|
||||||
|
"sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b",
|
||||||
|
"sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8",
|
||||||
|
"sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233",
|
||||||
|
"sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6",
|
||||||
|
"sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727",
|
||||||
|
"sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f",
|
||||||
|
"sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38",
|
||||||
|
"sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4",
|
||||||
|
"sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626",
|
||||||
|
"sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d",
|
||||||
|
"sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6",
|
||||||
|
"sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63",
|
||||||
|
"sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f",
|
||||||
|
"sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41",
|
||||||
|
"sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1",
|
||||||
|
"sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d",
|
||||||
|
"sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9",
|
||||||
|
"sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a",
|
||||||
|
"sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==7.2.0"
|
||||||
|
},
|
||||||
|
"pyasn1": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||||
|
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||||
|
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||||
|
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||||
|
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||||
|
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||||
|
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||||
|
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||||
|
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||||
|
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||||
|
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||||
|
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||||
|
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||||
|
],
|
||||||
|
"version": "==0.4.8"
|
||||||
|
},
|
||||||
|
"pycparser": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||||
|
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.20"
|
||||||
|
},
|
||||||
|
"pycryptodomex": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:06f5a458624c9b0e04c0086c7f84bcc578567dab0ddc816e0476b3057b18339f",
|
||||||
|
"sha256:1714675fb4ac29a26ced38ca22eb8ffd923ac851b7a6140563863194d7158422",
|
||||||
|
"sha256:17272d06e4b2f6455ee2cbe93e8eb50d9450a5dc6223d06862ee1ea5d1235861",
|
||||||
|
"sha256:2199708ebeed4b82eb45b10e1754292677f5a0df7d627ee91ea01290b9bab7e6",
|
||||||
|
"sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda",
|
||||||
|
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
|
||||||
|
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
|
||||||
|
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
|
||||||
|
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
|
||||||
|
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
|
||||||
|
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
|
||||||
|
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
|
||||||
|
"sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68",
|
||||||
|
"sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259",
|
||||||
|
"sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48",
|
||||||
|
"sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2",
|
||||||
|
"sha256:93a75d1acd54efed314b82c952b39eac96ce98d241ad7431547442e5c56138aa",
|
||||||
|
"sha256:9fd758e5e2fe02d57860b85da34a1a1e7037155c4eadc2326fc7af02f9cae214",
|
||||||
|
"sha256:a2bc4e1a2e6ca3a18b2e0be6131a23af76fecb37990c159df6edc7da6df913e3",
|
||||||
|
"sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71",
|
||||||
|
"sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c",
|
||||||
|
"sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761",
|
||||||
|
"sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00",
|
||||||
|
"sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34",
|
||||||
|
"sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489",
|
||||||
|
"sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060",
|
||||||
|
"sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594",
|
||||||
|
"sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677",
|
||||||
|
"sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
|
||||||
|
"sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==3.9.8"
|
||||||
|
},
|
||||||
|
"pydub": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:25fdfbbfd4c69363006a27c7bd2346c4b886a0dd3da264c14d858b71a9593284",
|
||||||
|
"sha256:630c68bfff9bb27cbc5e1f02923f717c3bc5f4d73fd685fda08b6ce90f76dc69"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.24.1"
|
||||||
|
},
|
||||||
|
"pyjwkest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
|
||||||
|
],
|
||||||
|
"version": "==1.4.2"
|
||||||
},
|
},
|
||||||
"pyjwt": {
|
"pyjwt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -175,51 +627,276 @@
|
|||||||
},
|
},
|
||||||
"pyrsistent": {
|
"pyrsistent": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2"
|
"sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"
|
||||||
],
|
],
|
||||||
"version": "==0.14.11"
|
"version": "==0.16.0"
|
||||||
|
},
|
||||||
|
"python-dateutil": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||||
|
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.8.1"
|
||||||
|
},
|
||||||
|
"python-editor": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
|
||||||
|
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
|
||||||
|
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8",
|
||||||
|
"sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77",
|
||||||
|
"sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"
|
||||||
|
],
|
||||||
|
"version": "==1.0.4"
|
||||||
|
},
|
||||||
|
"python-engineio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:133bdb5fb89f43a53f8612fb1ddbb3a453318713dea18a9ecf5346ed0c0f793c",
|
||||||
|
"sha256:41353c2539493e9e30e0e75e53f9cbb670f09a5ebcf82fe738081a9ba28fe55c"
|
||||||
|
],
|
||||||
|
"version": "==3.13.1"
|
||||||
|
},
|
||||||
|
"python-jose": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4e4192402e100b5fb09de5a8ea6bcc39c36ad4526341c123d401e2561720335b",
|
||||||
|
"sha256:67d7dfff599df676b04a996520d9be90d6cdb7e6dd10b4c7cacc0c3e2e92f2be"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"python-socketio": {
|
||||||
|
"extras": [
|
||||||
|
"client"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:358d8fbbc029c4538ea25bcaa283e47f375be0017fcba829de8a3a731c9df25a",
|
||||||
|
"sha256:d437f797c44b6efba2f201867cf02b8c96b97dff26d4e4281ac08b45817cd522"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.6.0"
|
||||||
|
},
|
||||||
|
"pythonping": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0338c6cbb03b7318b38d33c42526faf7f71dd25d9a627b3cf29e741e26a1f7ac",
|
||||||
|
"sha256:94f7226c0d98cc0bd739c5f00f65aeffa1e0e7bbbf01eff529c912a25dfdf7fc"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.11"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
||||||
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
|
||||||
],
|
],
|
||||||
"version": "==2018.9"
|
"version": "==2020.1"
|
||||||
},
|
},
|
||||||
"relativetimebuilder": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5cc415b539d18a20e09a600cf7ba7199eda7b365d13aaaf9ffbbaa26cfb8062a",
|
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||||
"sha256:8b11e6fa6d6d4a09c61cfa9dadae4ea640bf10818e0991874d33452c0aeff2d7"
|
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||||
],
|
],
|
||||||
"version": "==0.2.0"
|
"version": "==2.24.0"
|
||||||
|
},
|
||||||
|
"rsa": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
|
||||||
|
"sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5' and python_version < '4'",
|
||||||
|
"version": "==4.6"
|
||||||
|
},
|
||||||
|
"scapy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:e2f8d11f6a941c14a789ae8b236b27bd634681f1b29b5e893861e284d234f6b0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.4.3"
|
||||||
|
},
|
||||||
|
"simpleaudio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05b63da515f5fc7c6f40e4d9673d22239c5e03e2bda200fc09fd21c185d73713",
|
||||||
|
"sha256:67348e3d3ccbae73bd126beed7f1e242976889620dbc6974c36800cd286430fc",
|
||||||
|
"sha256:691c88649243544db717e7edf6a9831df112104e1aefb5f6038a5d071e8cf41d",
|
||||||
|
"sha256:86f1b0985629852afe67259ac6c24905ca731cb202a6e96b818865c56ced0c27",
|
||||||
|
"sha256:f1a4fe3358429b2ea3181fd782e4c4fff5c123ca86ec7fc29e01ee9acd8a227a",
|
||||||
|
"sha256:f346a4eac9cdbb1b3f3d0995095b7e86c12219964c022f4d920c22f6ca05fb4c",
|
||||||
|
"sha256:f68820297ad51577e3a77369e7e9b23989d30d5ae923bf34c92cf983c04ade04"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.4"
|
||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
"version": "==1.12.0"
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.15.0"
|
||||||
|
},
|
||||||
|
"socketio-client": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:64cd84fba79cf97f28c11e64d1fc42a2221f2d7a4fada05ab381e2d73d74d2c1"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7.2"
|
||||||
|
},
|
||||||
|
"soupsieve": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55",
|
||||||
|
"sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==2.0.1"
|
||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:11ead7047ff3f394ed0d4b62aded6c5d970a9b718e1dc6add9f5e79442cc5b14"
|
"sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e",
|
||||||
|
"sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772",
|
||||||
|
"sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7",
|
||||||
|
"sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf",
|
||||||
|
"sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98",
|
||||||
|
"sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864",
|
||||||
|
"sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9",
|
||||||
|
"sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1",
|
||||||
|
"sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd",
|
||||||
|
"sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4",
|
||||||
|
"sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1",
|
||||||
|
"sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c",
|
||||||
|
"sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8",
|
||||||
|
"sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e",
|
||||||
|
"sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce",
|
||||||
|
"sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1",
|
||||||
|
"sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5",
|
||||||
|
"sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe",
|
||||||
|
"sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413",
|
||||||
|
"sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3",
|
||||||
|
"sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284",
|
||||||
|
"sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1",
|
||||||
|
"sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7",
|
||||||
|
"sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299",
|
||||||
|
"sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33",
|
||||||
|
"sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d",
|
||||||
|
"sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274",
|
||||||
|
"sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"
|
||||||
],
|
],
|
||||||
"version": "==1.3.0"
|
"index": "pypi",
|
||||||
|
"version": "==1.3.18"
|
||||||
},
|
},
|
||||||
"webargs": {
|
"sqlalchemy-migrate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:10438164b41b81abe45b299eb182580f7bc6bcdbc864b0cbd62845bb6bab424d",
|
"sha256:0bc02e292a040ade5e35a01d3ea744119e1309cdddb704fdb99bac13236614f8",
|
||||||
"sha256:3bed01136ea4a7d1468a54f6c3925d133872a83a2144e83a94f484731576bc58",
|
"sha256:e5d2348db19a5062132d93e3b4d9e7644af552fffbec4c78cc5358f848d2f6c1"
|
||||||
"sha256:494044344b5673e3624621d0e9d14d5dc01dd05c0b5b8952febc80a4f80181f6"
|
|
||||||
],
|
],
|
||||||
"version": "==5.1.2"
|
"index": "pypi",
|
||||||
|
"version": "==0.13.0"
|
||||||
|
},
|
||||||
|
"sqlparse": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
|
||||||
|
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==0.3.1"
|
||||||
|
},
|
||||||
|
"ssh2-python": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:054f2cab611ddca34095eb78d1ae1f6e29521678c82f0de17f696072a0b924fe",
|
||||||
|
"sha256:097d74684172f163d5025aef316987c1c7acf852d9fb75e4735720c8690adf87",
|
||||||
|
"sha256:0f4555b79e19ca6fdd84a74e85152d375a921d167ed9dc680d05f1f9f5dfc463",
|
||||||
|
"sha256:0fb3d9eca454b2874748c69e7e836ea5c6544f1b7acd87d8a876f99f618c806c",
|
||||||
|
"sha256:180c2728796f777a5856b23e5e1656a15a3e8602b4270348f892599ae5426f45",
|
||||||
|
"sha256:49aaa9d48cf52798d89f466a5e774e9dab8b76dc380b90645fd8d5c6622df177",
|
||||||
|
"sha256:4b5ba1390ff5e8faa6b2409df750e4650de3934c3b76ad707d7b54b477600226",
|
||||||
|
"sha256:57a4452dfc5c7f414de14e32f29e21f680fb8d7bdf94306e5f6bc8c2a235fa7a",
|
||||||
|
"sha256:584f753f0401217a55e70d2ae97a7e5220f4d37993a1bba275c4c5e110d87193",
|
||||||
|
"sha256:692cf27ddc5583da9b07b0f0348f0577579f8d798a0b8e4bf495b8d7f19fc854",
|
||||||
|
"sha256:6dc75bacc37de63aa9c86f9982642982c3a8103297fbbb73543a76e3db9abc6a",
|
||||||
|
"sha256:7a029275e62a1e31333ee7f2f151f661dedfac0345f1108e1a8d0b62e08ddf9b",
|
||||||
|
"sha256:7a3d761c669392e778c24a0eabcf6f1ba48ad68bb3b82a2d98235f56500bc8b0",
|
||||||
|
"sha256:8632684aae2f523e603381cf51a1da134fd5937cf0ae9650b1b9e7b4f328c133",
|
||||||
|
"sha256:8acab66dd73554d2c68abaccf96cdddfc10449ad6c7c273ac4b7a91779d414b2",
|
||||||
|
"sha256:9410b63c2361b2b65a55b5e8dc8187270de0f81e11864a427ad52b82b926bcc9",
|
||||||
|
"sha256:abe29ad01ed2ac4465e61c8de42b6850fca9c87d8792686a2de7c7d06c6eb72d",
|
||||||
|
"sha256:de151500a2a027a29bca3d7196c76008b41bb97db29378f29c4651ba1bcb00e8",
|
||||||
|
"sha256:debc56711643b82ab60a9c8b8cbf2978b65c0293aa7e53557a3c0d1fc57799cf",
|
||||||
|
"sha256:f508144d684b3c3c0f264ce67058f05ae3729ae77cc3a8f2051b442e01594849"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.18.0.post1"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6485a4bcb84629341a5fd1587fe3ac4887daa4741f0c8a1d01b9c3c8a263afe7",
|
||||||
|
"sha256:8eb365fc15039b19b728bd4e6e85fb7daf24e7aeeec6a15a666f97484c564005"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"tatsu": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0adbf7189a8c4f9a882b442f7b8ed6c6ab3baae37057db0e96b6888daacffad0",
|
||||||
|
"sha256:3a043490e577632a05374b5033646bbc26cbb17386df81735a569ecbd45d934b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==5.5.0"
|
||||||
|
},
|
||||||
|
"tempita": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c"
|
||||||
|
],
|
||||||
|
"version": "==0.5.2"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
|
||||||
|
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
|
||||||
|
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
|
||||||
|
],
|
||||||
|
"version": "==3.7.4.2"
|
||||||
|
},
|
||||||
|
"tzlocal": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44",
|
||||||
|
"sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"
|
||||||
|
],
|
||||||
|
"version": "==2.1"
|
||||||
|
},
|
||||||
|
"update": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a25522b4bf60e3e3c1a3ff3ca3a4f5a328ac4b8ff400fdc9614483147e313323"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.0.1"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||||
|
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
|
"version": "==1.25.10"
|
||||||
|
},
|
||||||
|
"webencodings": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||||
|
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||||
|
],
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
|
"websocket-client": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
|
||||||
|
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.57.0"
|
||||||
},
|
},
|
||||||
"werkzeug": {
|
"werkzeug": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
|
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
|
||||||
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
|
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
|
||||||
],
|
],
|
||||||
"version": "==0.14.1"
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
|||||||
7
README.md
Normal file
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Backend code for LRC
|
||||||
|
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
|
||||||
|
- python > 3.6
|
||||||
|
- libasound2-dev
|
||||||
21
__init__.py
21
__init__.py
@@ -1,21 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Backend base module
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
jwt_auth = HTTPTokenAuth()
|
|
||||||
basic_auth = HTTPBasicAuth()
|
|
||||||
multi_auth = MultiAuth(basic_auth, jwt_auth)
|
|
||||||
|
|
||||||
from .serve_frontend import fe_bp
|
|
||||||
from .api import api_bp
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.register_blueprint(api_bp)
|
|
||||||
app.register_blueprint(fe_bp)
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
13
__main__.py
13
__main__.py
@@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright (c) 2019. Tobias Kurze
|
|
||||||
from backend import app
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app.run(debug=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from flask import Blueprint
|
|
||||||
from flask_restplus import Api
|
|
||||||
|
|
||||||
api_authorizations = {
|
|
||||||
'apikey': {
|
|
||||||
'type': 'apiKey',
|
|
||||||
'in': 'header',
|
|
||||||
'name': 'X-API-KEY'
|
|
||||||
},
|
|
||||||
'basicAuth': {
|
|
||||||
'type': 'basic',
|
|
||||||
'scheme': 'basic'
|
|
||||||
},
|
|
||||||
'bearerAuth': {
|
|
||||||
'type': 'apiKey',
|
|
||||||
'scheme': 'bearer',
|
|
||||||
'name': 'Authorization',
|
|
||||||
'in': 'header'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
|
||||||
api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API',
|
|
||||||
description='The Vue Test API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
|
|
||||||
|
|
||||||
from .example_api import *
|
|
||||||
202
backend/__init__.py
Normal file
202
backend/__init__.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Backend base module
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from functools import wraps
|
||||||
|
from io import StringIO
|
||||||
|
from logging.config import dictConfig
|
||||||
|
from logging.handlers import MemoryHandler
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import coloredlogs as coloredlogs
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
from flask import Flask, jsonify, abort
|
||||||
|
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
|
||||||
|
from flask_jwt_extended import JWTManager, decode_token, get_jwt_identity
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_cors import CORS
|
||||||
|
from backend.config import Config
|
||||||
|
|
||||||
|
__author__ = "Tobias Kurze"
|
||||||
|
__copyright__ = "Copyright 2019, Tobias Kurze, KIT"
|
||||||
|
__credits__ = ["Tobias Kurze"]
|
||||||
|
__license__ = ""
|
||||||
|
__version__ = "0.9.0"
|
||||||
|
__maintainer__ = "Tobias Kurze"
|
||||||
|
__email__ = "it@t-kurze.de"
|
||||||
|
# __status__ = "Production"
|
||||||
|
__status__ = "Development"
|
||||||
|
|
||||||
|
from .tools.send_mail import get_smtp_default_handler
|
||||||
|
|
||||||
|
dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'formatters': {
|
||||||
|
'default': {
|
||||||
|
'format': '[%(asctime)s] {%(threadName)s} %(levelname)s in %(module)s, line %(lineno)d: %(message)s',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'wsgi': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||||
|
'formatter': 'default'
|
||||||
|
},
|
||||||
|
'root_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': Config.ROOT_LOG_FILE,
|
||||||
|
'when': 'd',
|
||||||
|
'interval': 1,
|
||||||
|
'backupCount': 3,
|
||||||
|
'formatter': 'default',
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': Config.LOG_FILE,
|
||||||
|
'when': 'd',
|
||||||
|
'interval': 1,
|
||||||
|
'backupCount': 5,
|
||||||
|
'formatter': 'default',
|
||||||
|
},
|
||||||
|
'errors_file': {
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': Config.ERROR_LOG_FILE,
|
||||||
|
'when': 'd',
|
||||||
|
'interval': 1,
|
||||||
|
'backupCount': 5,
|
||||||
|
'level': 'ERROR',
|
||||||
|
'formatter': 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'lrc': {
|
||||||
|
'level': Config.LOG_LEVEL,
|
||||||
|
'handlers': ['wsgi', 'file', 'errors_file']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'handlers': ['root_file', 'errors_file']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
main_logger = logging.getLogger("lrc")
|
||||||
|
|
||||||
|
# following might be dangerous, as buffer might be filled without a mechanism to empty it
|
||||||
|
smtp_error_handler = get_smtp_default_handler(subject="Warnings, errors and worse...!")
|
||||||
|
mem_handler = MemoryHandler(capacity=10, flushLevel=logging.ERROR, target=smtp_error_handler)
|
||||||
|
mem_handler.setLevel(logging.WARNING)
|
||||||
|
# error_log_stream = StringIO()
|
||||||
|
# error_log_stream_handler = logging.StreamHandler(stream=error_log_stream)
|
||||||
|
# error_log_stream_handler.setLevel(logging.ERROR)
|
||||||
|
# main_logger.addHandler(error_log_stream_handler)
|
||||||
|
|
||||||
|
coloredlogs.install(level='DEBUG', logger=main_logger)
|
||||||
|
|
||||||
|
|
||||||
|
class LrcException(Exception):
|
||||||
|
def __init__(self, message_or_exception: Union[str, Exception], html_code: int = None):
|
||||||
|
if isinstance(message_or_exception, str):
|
||||||
|
super().__init__(message_or_exception)
|
||||||
|
self.type = None
|
||||||
|
else:
|
||||||
|
super().__init__(str(message_or_exception))
|
||||||
|
self.type = type(message_or_exception)
|
||||||
|
self.html_code = html_code
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.type is None:
|
||||||
|
msg = "LRC Exception: \"{}\"".format(', '.join(super().args))
|
||||||
|
else:
|
||||||
|
msg = "LRC Exception: (original Exception: {}) \"{}\"".format(self.type, ', '.join(super().args))
|
||||||
|
if self.html_code is not None:
|
||||||
|
msg += " (HTML Code: {})".format(self.html_code)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
if os.environ.get('FLASK_ENV', None) in ["development", "dev"]:
|
||||||
|
print("#using backend.config.DevelopmentConfig")
|
||||||
|
app.config.from_object('backend.config.DevelopmentConfig')
|
||||||
|
elif os.environ.get('FLASK_ENV', None) in ["test", "testing"]:
|
||||||
|
print("#using backend.config.TestingConfig")
|
||||||
|
app.config.from_object('backend.config.TestingConfig')
|
||||||
|
else:
|
||||||
|
app.config.from_object('backend.config.Config')
|
||||||
|
print("#using backend.config.Config")
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
|
||||||
|
# flask_jwt_extended: to be used usually by API
|
||||||
|
jwt_extended = JWTManager(app)
|
||||||
|
# this is another library.... this is (probaby ->verify) used to check JWTs provided by external sources (KIT, etc.)
|
||||||
|
jwt_auth = HTTPTokenAuth('Bearer')
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_auth.verify_token
|
||||||
|
def verify_token(token):
|
||||||
|
"""This function (and HTTPTokenAuth('Bearer')) has been defined to be used together with MultiAuth. For API calls
|
||||||
|
solely using JWT authentication, jwt_required of flask_jwt_extended should be used directly."""
|
||||||
|
app.logger.info(token)
|
||||||
|
try:
|
||||||
|
decoded = decode_token(token)
|
||||||
|
except jwt.exceptions.DecodeError as e:
|
||||||
|
app.logger.warn("Could not verify token: {}".format(str(e)))
|
||||||
|
return False
|
||||||
|
except jwt.exceptions.ExpiredSignatureError as e:
|
||||||
|
app.logger.warn("Could not verify token: {}".format(str(e)))
|
||||||
|
return False
|
||||||
|
app.logger.info(decoded)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
basic_auth = HTTPBasicAuth()
|
||||||
|
multi_auth = MultiAuth(basic_auth, jwt_auth)
|
||||||
|
|
||||||
|
from backend.auth import oidc_auth, auth_bp
|
||||||
|
|
||||||
|
try:
|
||||||
|
oidc_auth.init_app(app)
|
||||||
|
except requests.exceptions.ConnectionError as err:
|
||||||
|
app.logger.error("Could not connect to OIDC!!", err)
|
||||||
|
|
||||||
|
# oidc_multi_auth = MultiAuth(oidc_auth, jwt_auth) <- can't work as OIDCAuthentication not implementing HTTPAuth
|
||||||
|
|
||||||
|
from .serve_frontend import fe_bp
|
||||||
|
from .api import auth_api_bp, api_v1, api_bp
|
||||||
|
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
app.register_blueprint(auth_api_bp)
|
||||||
|
app.register_blueprint(api_bp)
|
||||||
|
app.register_blueprint(fe_bp)
|
||||||
|
|
||||||
|
CORS(app)
|
||||||
|
CORS(api_bp)
|
||||||
|
CORS(auth_api_bp)
|
||||||
|
|
||||||
|
logging.getLogger('flask_cors').level = logging.DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
# Fix jwt_extended by 'duck typing' error handlers
|
||||||
|
# jwt_extended._set_error_handler_callbacks(api_v1) # removed for the moment, might raise new (old) problems
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_extended.invalid_token_loader
|
||||||
|
def unauthorized_jwt(token):
|
||||||
|
main_logger.info("Unauthorized access; invalid token provided: {}".format(token))
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_extended.expired_token_loader
|
||||||
|
def unauthorized_jwt(token):
|
||||||
|
main_logger.info("Unauthorized access; expired token provided: {}".format(token))
|
||||||
|
abort(401)
|
||||||
111
backend/__main__.py
Normal file
111
backend/__main__.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
import logging
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from jinja2.exceptions import TemplateNotFound
|
||||||
|
|
||||||
|
from backend import app, db, main_logger
|
||||||
|
from backend.cron import get_default_scheduler, add_default_jobs, async_permanent_cron_recorder_checker
|
||||||
|
from backend.models import *
|
||||||
|
from backend.models import room_model, recorder_model, RecorderCommand, Recorder
|
||||||
|
from backend.recorder_adapters import get_defined_recorder_adapters
|
||||||
|
from backend.tools.model_updater import update_recorder_models_database, create_default_recorders, add_test_recorder
|
||||||
|
from backend.websocket.base import WebSocketBase
|
||||||
|
|
||||||
|
|
||||||
|
def _start_initial_recorder_state_update(run_in_thread=True):
|
||||||
|
if run_in_thread:
|
||||||
|
thread = threading.Thread(target=async_permanent_cron_recorder_checker.check_object_state, args=())
|
||||||
|
thread.start()
|
||||||
|
else:
|
||||||
|
async_permanent_cron_recorder_checker.check_object_state() # initial check of all recorders
|
||||||
|
|
||||||
|
|
||||||
|
def _create_and_start_default_scheduler():
|
||||||
|
print("Starting Scheduler")
|
||||||
|
scheduler = get_default_scheduler()
|
||||||
|
add_default_jobs(scheduler)
|
||||||
|
scheduler.start()
|
||||||
|
return scheduler
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
print("starting ...")
|
||||||
|
|
||||||
|
# db.drop_all()
|
||||||
|
# db.create_all()
|
||||||
|
|
||||||
|
# Recorder()
|
||||||
|
room_model.pre_fill_table()
|
||||||
|
update_recorder_models_database(drop=False)
|
||||||
|
create_default_recorders()
|
||||||
|
add_test_recorder()
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.create_all()
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical(e)
|
||||||
|
|
||||||
|
scheduler = _create_and_start_default_scheduler()
|
||||||
|
# _start_initial_recorder_state_update(run_in_thread=False)
|
||||||
|
|
||||||
|
print("Server Name: {}".format(app.config.get("SERVER_NAME", None)))
|
||||||
|
|
||||||
|
wsb = WebSocketBase()
|
||||||
|
|
||||||
|
if app.config.get("USE_SSL", False):
|
||||||
|
try:
|
||||||
|
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
||||||
|
context.load_cert_chain(app.config.get("CERT", 'cert.pem'), app.config.get("KEY", 'key.pem'))
|
||||||
|
print("using ssl context!")
|
||||||
|
#app.run(debug=True, ssl_context=context, threaded=True,
|
||||||
|
# host=app.config.get("HOST", "0.0.0.0"),
|
||||||
|
# port=app.config.get("PORT", 5443)
|
||||||
|
# )
|
||||||
|
print("running websocket...(replaces normal app.run()")
|
||||||
|
|
||||||
|
wsb.start_websocket(debug=app.config.get("DEBUG", False),
|
||||||
|
host=app.config.get("HOST", "0.0.0.0"),
|
||||||
|
port=app.config.get("PORT", 5443),
|
||||||
|
ssl_context=context
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Could not find cert/key.pem!")
|
||||||
|
#app.run(debug=True, threaded=True,
|
||||||
|
# host=app.config.get("HOST", None),
|
||||||
|
# port=app.config.get("PORT", 5443)
|
||||||
|
# )
|
||||||
|
print("running websocket...(replaces normal app.run()")
|
||||||
|
wsb.start_websocket(debug=app.config.get("DEBUG", False),
|
||||||
|
host=app.config.get("HOST", "0.0.0.0"),
|
||||||
|
port=app.config.get("PORT", 5443)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("running websocket...(replaces normal app.run()")
|
||||||
|
wsb.start_websocket(debug=app.config.get("DEBUG", False),
|
||||||
|
host=app.config.get("HOST", "0.0.0.0"),
|
||||||
|
port=app.config.get("PORT", 5443)
|
||||||
|
)
|
||||||
|
# print("running web app...")
|
||||||
|
# app.run(debug=True, host="0.0.0.0", threaded=True)
|
||||||
|
wsb.send_test_msg()
|
||||||
|
print("running!")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
user_in = input("Type >exit< to quit.")
|
||||||
|
if user_in == "exit" or user_in == ">exit<":
|
||||||
|
break
|
||||||
|
|
||||||
|
scheduler.shutdown()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
||||||
98
backend/api/__init__.py
Normal file
98
backend/api/__init__.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from flask import Blueprint, abort
|
||||||
|
from flask_restx import Api, Namespace
|
||||||
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
|
api_authorizations = {
|
||||||
|
'apikey': {
|
||||||
|
'type': 'apiKey',
|
||||||
|
'in': 'header',
|
||||||
|
'name': 'X-API-KEY'
|
||||||
|
},
|
||||||
|
'basicAuth': {
|
||||||
|
'type': 'basic',
|
||||||
|
'scheme': 'basic'
|
||||||
|
},
|
||||||
|
'bearerAuth': {
|
||||||
|
'type': 'apiKey',
|
||||||
|
'scheme': 'bearer',
|
||||||
|
'name': 'Authorization',
|
||||||
|
'in': 'header'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||||
|
api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API',
|
||||||
|
description='The Vue Test API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
|
||||||
|
|
||||||
|
api_user = Namespace('user', description="User management namespace", authorizations=api_authorizations)
|
||||||
|
api_group = Namespace('group', description="Group management namespace", authorizations=api_authorizations)
|
||||||
|
api_permissions = Namespace('permissions', description="Permissions management namespace", authorizations=api_authorizations)
|
||||||
|
api_room = Namespace('room', description="Room management namespace", authorizations=api_authorizations)
|
||||||
|
api_recorder = Namespace('recorder', description="Recorder management namespace", authorizations=api_authorizations)
|
||||||
|
api_state = Namespace('state', description="Object state management namespace", authorizations=api_authorizations)
|
||||||
|
api_virtual_command = Namespace('virtual_command', description="Virtual command namespace",
|
||||||
|
authorizations=api_authorizations)
|
||||||
|
api_cron_job = Namespace('cron_job', description="Cron job namespace",
|
||||||
|
authorizations=api_authorizations)
|
||||||
|
api_control = Namespace('control', description="Control namespace",
|
||||||
|
authorizations=api_authorizations)
|
||||||
|
|
||||||
|
api_v1.add_namespace(api_user)
|
||||||
|
api_v1.add_namespace(api_group)
|
||||||
|
api_v1.add_namespace(api_permissions)
|
||||||
|
api_v1.add_namespace(api_room)
|
||||||
|
api_v1.add_namespace(api_recorder)
|
||||||
|
api_v1.add_namespace(api_state)
|
||||||
|
api_v1.add_namespace(api_virtual_command)
|
||||||
|
api_v1.add_namespace(api_cron_job)
|
||||||
|
api_v1.add_namespace(api_control)
|
||||||
|
|
||||||
|
auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth')
|
||||||
|
auth_api_v1 = Api(auth_api_bp, prefix="/v1", version='0.1', title='Auth API',
|
||||||
|
description='Auth API', doc='/v1/doc/', authorizations=api_authorizations, security='bearerAuth')
|
||||||
|
|
||||||
|
auth_api_providers_ns = Namespace('providers')
|
||||||
|
auth_api_register_ns = Namespace('register')
|
||||||
|
auth_api_v1.add_namespace(auth_api_providers_ns)
|
||||||
|
auth_api_v1.add_namespace(auth_api_register_ns)
|
||||||
|
# user_api_bp = Blueprint('user_api', __name__, url_prefix='/api/user')
|
||||||
|
# group_api_bp = Blueprint('group_api', __name__, url_prefix='/api/group')
|
||||||
|
|
||||||
|
from .example_api import *
|
||||||
|
from .auth_api import *
|
||||||
|
from .user_api import *
|
||||||
|
from .permission_api import *
|
||||||
|
from .group_api import *
|
||||||
|
from .room_api import *
|
||||||
|
from .recorder_api import *
|
||||||
|
from .state_api import *
|
||||||
|
from .control_api import *
|
||||||
|
from .virtual_command_api import *
|
||||||
|
|
||||||
|
|
||||||
|
# from .group_api import *
|
||||||
|
|
||||||
|
"""
|
||||||
|
@api_v1.errorhandler(InternalServerError)
|
||||||
|
def handle_500(e):
|
||||||
|
original = getattr(e, "original_exception", None)
|
||||||
|
|
||||||
|
if original is None:
|
||||||
|
# direct 500 error, such as abort(500)
|
||||||
|
api_v1.abort(500)
|
||||||
|
|
||||||
|
# wrapped unhandled error
|
||||||
|
#return render_template("500_unhandled.html", e=original), 500
|
||||||
|
api_v1.abort(500)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@api_bp.route('/<path:path>')
|
||||||
|
def catch_all_api(path):
|
||||||
|
"""
|
||||||
|
Default 404 response for undefined paths in API.
|
||||||
|
:param path:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
abort(404)
|
||||||
211
backend/api/auth_api.py
Normal file
211
backend/api/auth_api.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import jwt
|
||||||
|
from flask import request, jsonify, current_app, url_for, Response, session, redirect, make_response
|
||||||
|
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_refresh_token_required, get_jwt_identity, \
|
||||||
|
get_raw_jwt, jwt_required
|
||||||
|
from functools import wraps
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from flask_login import logout_user, login_user
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from flask_restx import Resource, fields, abort, inputs
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from werkzeug.routing import BuildError
|
||||||
|
|
||||||
|
from backend import db, app, jwt_extended
|
||||||
|
from backend.api import auth_api_bp, auth_api_providers_ns, auth_api_register_ns
|
||||||
|
from backend.api.models import user_model
|
||||||
|
from backend.auth import AUTH_PROVIDERS, oidc_auth
|
||||||
|
from backend.auth.oidc_config import PROVIDER_NAME
|
||||||
|
from backend.models.user_model import User, Group, BlacklistToken
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.api.auth")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_api_bp.route('/providers', methods=('GET',))
|
||||||
|
def get_auth_providers():
|
||||||
|
providers = dict()
|
||||||
|
for p in AUTH_PROVIDERS:
|
||||||
|
provider = dict(AUTH_PROVIDERS[p])
|
||||||
|
try:
|
||||||
|
provider["url"] = url_for(AUTH_PROVIDERS[p]["url"])
|
||||||
|
except BuildError:
|
||||||
|
provider["url"] = AUTH_PROVIDERS[p]["url"]
|
||||||
|
providers[p] = provider
|
||||||
|
return jsonify(providers)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_api_providers_ns.route('/')
|
||||||
|
class AuthProviders(Resource):
|
||||||
|
def get(self):
|
||||||
|
return get_auth_providers()
|
||||||
|
|
||||||
|
|
||||||
|
@auth_api_register_ns.route('/')
|
||||||
|
@auth_api_register_ns.expect(auth_api_register_ns.model('RegisterModel', {
|
||||||
|
'nickname': fields.String(required=False, description='The user\'s nickname'),
|
||||||
|
'first_name': fields.String(required=False, description='The user\'s first name'),
|
||||||
|
'last_name': fields.String(required=False, description='The user\'s last name'),
|
||||||
|
'lang': fields.String(required=False, description='The user\'s preferred language'),
|
||||||
|
'timezone': fields.String(required=False, description='The user\'s preferred timezone'),
|
||||||
|
'email': fields.String(required=True, type=inputs.email(), description='The user\'s e-mail address'),
|
||||||
|
'password': fields.String(required=False, description='The group\'s name')
|
||||||
|
}))
|
||||||
|
class Registration(Resource):
|
||||||
|
@auth_api_register_ns.marshal_list_with(user_model)
|
||||||
|
def post(self):
|
||||||
|
print("in registration")
|
||||||
|
data = request.get_json()
|
||||||
|
try:
|
||||||
|
user = User(**data)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
pprint(user.to_dict())
|
||||||
|
return user, 201
|
||||||
|
except IntegrityError as e:
|
||||||
|
abort(400, message=str(e).split('\n')[0].split(')')[1].strip())
|
||||||
|
except AssertionError as e:
|
||||||
|
abort(400, message=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@auth_api_bp.route('/login', methods=('GET', 'POST',))
|
||||||
|
def login():
|
||||||
|
print("login")
|
||||||
|
print(request)
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'message': 'Invalid request data', 'authenticated': False}), 401
|
||||||
|
print(data)
|
||||||
|
user = User.authenticate(**data)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify({'message': 'Invalid credentials', 'authenticated': False}), 401
|
||||||
|
|
||||||
|
token = {
|
||||||
|
'access_token': create_access_token(identity=user, fresh=True),
|
||||||
|
'refresh_token': create_refresh_token(identity=user)
|
||||||
|
}
|
||||||
|
return jsonify(token), 200
|
||||||
|
|
||||||
|
|
||||||
|
# Endpoint for revoking the current users access token
|
||||||
|
@auth_api_bp.route('/logout', methods=['GET', 'DELETE'])
|
||||||
|
@jwt_required
|
||||||
|
def logout():
|
||||||
|
jti = get_raw_jwt()['jti']
|
||||||
|
db.session.add(BlacklistToken(token=jti))
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"msg": "Successfully logged out"}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# Endpoint for revoking the current users refresh token
|
||||||
|
@auth_api_bp.route('/logout2', methods=['GET', 'DELETE'])
|
||||||
|
@auth_api_bp.route('/revokeRefreshToken', methods=['GET', 'DELETE'])
|
||||||
|
@jwt_refresh_token_required
|
||||||
|
def logout2():
|
||||||
|
jti = get_raw_jwt()['jti']
|
||||||
|
db.session.add(BlacklistToken(token=jti))
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"msg": "Successfully logged out"}), 200
|
||||||
|
|
||||||
|
|
||||||
|
def check_and_create_groups(groups: Iterable[str]):
|
||||||
|
user_groups = []
|
||||||
|
for g in groups:
|
||||||
|
group = Group.get_by_name(g)
|
||||||
|
if group is None:
|
||||||
|
group = Group(name=g)
|
||||||
|
db.session.add(group)
|
||||||
|
user_groups.append(group)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return user_groups
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_retrieve_user_from_userinfo(userinfo):
|
||||||
|
try:
|
||||||
|
email = userinfo["email"]
|
||||||
|
except KeyError:
|
||||||
|
logger.error("email is missing in OIDC userinfo! Can't create user!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
pprint(userinfo)
|
||||||
|
user_groups = check_and_create_groups(groups=userinfo.get("memberOf", []))
|
||||||
|
user = User.get_by_identifier(email)
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
logger.info("user found -> update user")
|
||||||
|
pprint(user.to_dict())
|
||||||
|
user.first_name = userinfo.get("given_name", "")
|
||||||
|
user.last_name = userinfo.get("family_name", "")
|
||||||
|
user.external_user_id = userinfo.get("eduperson_principal_name", None)
|
||||||
|
for g in user_groups:
|
||||||
|
user.groups.append(g)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
user = User(email=email, first_name=userinfo.get("given_name", ""),
|
||||||
|
last_name=userinfo.get("family_name", ""), external_user=True,
|
||||||
|
groups=user_groups, external_user_id=userinfo.get("eduperson_principal_name", None))
|
||||||
|
|
||||||
|
logger.info("creating new user")
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@auth_api_bp.route('/oidc', methods=['GET'])
|
||||||
|
@auth_api_bp.route('/oidc/<redirect_url>', methods=['GET'])
|
||||||
|
@oidc_auth.oidc_auth(provider_name=PROVIDER_NAME)
|
||||||
|
def oidc(redirect_url=None):
|
||||||
|
logger.debug("oidc auth endpoint:")
|
||||||
|
user = create_or_retrieve_user_from_userinfo(flask.session['userinfo'])
|
||||||
|
if user is None:
|
||||||
|
logger.error(f"Could not authenticate: could not find or create user:\n{str(flask.session['userinfo'])}")
|
||||||
|
return "Could not authenticate: could not find or create user.", 401
|
||||||
|
if current_app.config.get("AUTH_RETURN_EXTERNAL_JWT", False):
|
||||||
|
token = jwt.encode(flask.session['id_token'], current_app.config['SECRET_KEY'])
|
||||||
|
else:
|
||||||
|
token = json.dumps({
|
||||||
|
'access_token': create_access_token(identity=user, fresh=True),
|
||||||
|
'refresh_token': create_refresh_token(identity=user)
|
||||||
|
})
|
||||||
|
logger.info("Token: {}".format(token))
|
||||||
|
if redirect_url is None:
|
||||||
|
redirect_url = request.headers.get("Referer")
|
||||||
|
if redirect_url is None:
|
||||||
|
redirect_url = request.args.get('redirect_url')
|
||||||
|
if redirect_url is None:
|
||||||
|
redirect_url = "/"
|
||||||
|
response = make_response(redirect(redirect_url))
|
||||||
|
response.set_cookie('tokens', base64.b64encode(token.encode('utf-8')))
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@auth_api_bp.route('/refresh', methods=['GET'])
|
||||||
|
@jwt_refresh_token_required
|
||||||
|
def refresh():
|
||||||
|
"""Refresh token endpoint. This will generate a new access token from
|
||||||
|
the refresh token, but will mark that access token as non-fresh,
|
||||||
|
as we do not actually verify a password in this endpoint."""
|
||||||
|
jwt_identity = get_jwt_identity()
|
||||||
|
user = User.get_by_identifier(jwt_identity)
|
||||||
|
logger.info("Refreshing token for " + str(user))
|
||||||
|
new_token = create_access_token(identity=user, fresh=False)
|
||||||
|
ret = {'access_token': new_token}
|
||||||
|
return jsonify(ret), 200
|
||||||
55
backend/api/control_api.py
Normal file
55
backend/api/control_api.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask_jwt_extended import jwt_required, get_current_user, get_jwt_claims
|
||||||
|
from flask_restx import fields, Resource
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
from backend.api import api_control, get_jwt_identity, Recorder, RecorderCommand, pprint
|
||||||
|
from backend.recorder_adapters.helpers import execute_recorder_command
|
||||||
|
|
||||||
|
control_command_response_model = api_control.model('Control Command Response', {
|
||||||
|
'time': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||||
|
'ok': fields.Boolean(required=True, description='Field indicating whether command execution was successful.'),
|
||||||
|
'output': fields.String(required=False, description='Command output in case of success'),
|
||||||
|
'error': fields.String(required=False, description='Error description in case of a problem.'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_control.route('')
|
||||||
|
class ControlCommand(Resource):
|
||||||
|
control_command_parser = api_control.parser()
|
||||||
|
control_command_parser.add_argument('recorder_id', type=int, default=1, required=True)
|
||||||
|
control_command_parser.add_argument('command_id', type=int, default=1, required=True)
|
||||||
|
control_command_parser.add_argument('parameters', default={}, type=dict, required=False,
|
||||||
|
location='json')
|
||||||
|
|
||||||
|
#@jwt_required
|
||||||
|
@api_control.doc('run_command')
|
||||||
|
@api_control.expect(control_command_parser)
|
||||||
|
@api_control.marshal_with(control_command_response_model, skip_none=False, code=201)
|
||||||
|
def post(self):
|
||||||
|
#print(get_current_user())
|
||||||
|
#print(get_jwt_identity())
|
||||||
|
#current_user = {'user': get_current_user(), 'claims': get_jwt_claims()}
|
||||||
|
#TODO: right check! (acl, etc.)
|
||||||
|
args = self.control_command_parser.parse_args()
|
||||||
|
recorder = Recorder.get_by_identifier(args.get('recorder_id'))
|
||||||
|
if recorder is None:
|
||||||
|
api_control.abort(404, "Recorder not found!")
|
||||||
|
command = RecorderCommand.get_by_identifier(args.get('command_id'))
|
||||||
|
if command is None:
|
||||||
|
api_control.abort(404, "Command not found!")
|
||||||
|
|
||||||
|
success, output = execute_recorder_command(recorder, command, args.get('parameters', None))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return {'time': datetime.utcnow(), 'output': output, 'ok': success}
|
||||||
|
return {'time': datetime.utcnow(), 'error': output, 'ok': success}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
|
||||||
|
import datetime
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from random import *
|
from random import *
|
||||||
from flask import jsonify, Blueprint
|
from flask import jsonify, Blueprint, request
|
||||||
from flask_restplus import Resource, reqparse
|
from flask_restx import Resource, reqparse
|
||||||
|
|
||||||
from backend import basic_auth, multi_auth
|
from backend import basic_auth, multi_auth, db, jwt_auth
|
||||||
from backend.api import api_v1, api_bp
|
from backend.api import api_v1, api_bp
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +22,18 @@ def random_number():
|
|||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/test_jwt')
|
||||||
|
@jwt_auth.login_required
|
||||||
|
def random_number_jwt():
|
||||||
|
"""
|
||||||
|
:return: a random number
|
||||||
|
"""
|
||||||
|
response = {
|
||||||
|
'randomNumber': randint(1, 100)
|
||||||
|
}
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
class HelloWorld(Resource):
|
class HelloWorld(Resource):
|
||||||
"""
|
"""
|
||||||
This is a test class.
|
This is a test class.
|
||||||
@@ -59,7 +75,7 @@ class SensorData_Handler(Resource):
|
|||||||
print("values...")
|
print("values...")
|
||||||
print(args['values'])
|
print(args['values'])
|
||||||
values = json.loads(args['values'])
|
values = json.loads(args['values'])
|
||||||
wasss_app.logger.info("vals: " + str(values) + " (len: " + str(len(values)) + ")")
|
app.logger.info("vals: " + str(values) + " (len: " + str(len(values)) + ")")
|
||||||
|
|
||||||
rough_geo_location = None
|
rough_geo_location = None
|
||||||
try:
|
try:
|
||||||
91
backend/api/group_api.py
Normal file
91
backend/api/group_api.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_restx import fields, Resource
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
from backend.api import api_group
|
||||||
|
from backend.models.user_model import Group
|
||||||
|
|
||||||
|
group_model = api_group.model('Group', {
|
||||||
|
'id': fields.String(required=False, description='The group\'s identifier'),
|
||||||
|
'name': fields.String(required=True, description='The group\'s name'),
|
||||||
|
'description': fields.String(required=False, description='The group\'s description'),
|
||||||
|
'users': fields.List(fields.Nested(api_group.model('group_member',
|
||||||
|
{'id': fields.Integer(), 'nickname': fields.String(),
|
||||||
|
'first_name': fields.String(), 'last_name': fields.String(),
|
||||||
|
'email': fields.String(), 'registered_on': fields.DateTime(),
|
||||||
|
'last_seen': fields.DateTime()})),
|
||||||
|
required=False, description='Group members.'),
|
||||||
|
'permissions': fields.List(fields.Nested(api_group.model('group_permissions',
|
||||||
|
{'id': fields.Integer,
|
||||||
|
'name': fields.String})),
|
||||||
|
required=False, description='Group permissions.'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_group.route('/<int:id>')
|
||||||
|
@api_group.response(404, 'Group not found')
|
||||||
|
@api_group.param('id', 'The group identifier')
|
||||||
|
class GroupResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_group.doc('get_group')
|
||||||
|
@api_group.marshal_with(group_model)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a user given its identifier"""
|
||||||
|
group = Group.get_by_id(id)
|
||||||
|
if group is not None:
|
||||||
|
return group
|
||||||
|
api_group.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_group.doc('delete_todo')
|
||||||
|
@api_group.response(204, 'Todo deleted')
|
||||||
|
def delete(self, id):
|
||||||
|
'''Delete a task given its identifier'''
|
||||||
|
group = Group.get_by_id(id)
|
||||||
|
if group is not None:
|
||||||
|
group.delete()
|
||||||
|
return '', 204
|
||||||
|
api_group.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_group.doc('update_group')
|
||||||
|
@api_group.expect(group_model)
|
||||||
|
@api_group.marshal_with(group_model)
|
||||||
|
def put(self, id):
|
||||||
|
'''Update a task given its identifier'''
|
||||||
|
group = Group.get_by_id(id)
|
||||||
|
if group is not None:
|
||||||
|
group.name = api_group["name"]
|
||||||
|
db.session.commit()
|
||||||
|
return group
|
||||||
|
api_group.abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@api_group.route('')
|
||||||
|
class GroupList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_group.doc('groups')
|
||||||
|
@api_group.marshal_list_with(group_model)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
List all groups
|
||||||
|
:return: groups
|
||||||
|
"""
|
||||||
|
return Group.get_all()
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_group.doc('create_group')
|
||||||
|
@api_group.expect(group_model)
|
||||||
|
@api_group.marshal_with(group_model, code=201)
|
||||||
|
def post(self):
|
||||||
|
group = Group(**api_group.payload)
|
||||||
|
db.session.add(group)
|
||||||
|
db.session.commit()
|
||||||
|
return group
|
||||||
124
backend/api/models.py
Normal file
124
backend/api/models.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
from flask_restx import fields
|
||||||
|
from backend.api import api_user, api_recorder, api_v1, api_state
|
||||||
|
|
||||||
|
generic_id_parser = api_v1.parser()
|
||||||
|
generic_id_parser.add_argument('id', type=str, required=True, store_missing=False)
|
||||||
|
|
||||||
|
user_model = api_user.model('User', {
|
||||||
|
'id': fields.String(required=True, description='The user\'s identifier'),
|
||||||
|
'first_name': fields.String(required=True, description='The user\'s first name'),
|
||||||
|
'last_name': fields.String(required=True, description='The user\'s last name'),
|
||||||
|
'email': fields.String(required=True, description='The user\'s email address'),
|
||||||
|
'nickname': fields.String(required=False, description='The user\'s nick name'),
|
||||||
|
'last_seen': fields.DateTime(required=False, description='Last time user logged in'),
|
||||||
|
'last_time_modified': fields.DateTime(required=False, description='Last time user was modified'),
|
||||||
|
'external_user': fields.Boolean(required=True, description='Indicates whether the user is external (OIDC) or not'),
|
||||||
|
'external_user_id': fields.String(required=False, description='External ID of a user (EPPN, etc.)'),
|
||||||
|
'role': fields.String(required=False, description='Role a user might have (in addition to group memberships)'),
|
||||||
|
'effective_permissions': fields.List(
|
||||||
|
fields.Nested(api_user.model('effective_permission',
|
||||||
|
{'id': fields.Integer(required=True),
|
||||||
|
'name': fields.String(required=True)
|
||||||
|
}),
|
||||||
|
required=False, description="List of permissions (groups + (optional) role).")
|
||||||
|
),
|
||||||
|
'groups': fields.List(
|
||||||
|
fields.Nested(api_user.model('user_group', {'id': fields.Integer(), 'name': fields.String()})),
|
||||||
|
required=False, description='Group memberships.'),
|
||||||
|
'favorite_recorders': fields.List(
|
||||||
|
fields.Nested(api_user.model('favorite_recorder',
|
||||||
|
{'id': fields.Integer(), 'name': fields.String(), 'offline': fields.Boolean(),
|
||||||
|
'created_at': fields.DateTime(), 'last_time_modified': fields.DateTime()})),
|
||||||
|
required=False, description='Favorite recorders.'),
|
||||||
|
})
|
||||||
|
|
||||||
|
recorder_model = api_recorder.model('Recorder', {
|
||||||
|
'id': fields.String(required=False, description='The recorder\'s identifier'),
|
||||||
|
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||||
|
'last_time_modified': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||||
|
'name': fields.String(min_length=3, required=True, description='The recorder\'s name'),
|
||||||
|
'model_name': fields.String(min_length=3, required=True,
|
||||||
|
description='The recorder\'s model name (might slightly '
|
||||||
|
'differ from actual name of the model)'),
|
||||||
|
'serial_number': fields.String(required=False, description='The recorder\'s serial number'),
|
||||||
|
'firmware_version': fields.String(required=False, description='The recorder\'s firmware'),
|
||||||
|
|
||||||
|
'description': fields.String(required=False, description='The recorder\'s description'),
|
||||||
|
'locked': fields.Boolean(required=False, description='Indicates whether the recorder settings can be altered'),
|
||||||
|
'lock_message': fields.String(required=False, description='Optional: message explaining lock state'),
|
||||||
|
'offline': fields.Boolean(required=False,
|
||||||
|
description='Should be set when recorder is disconnected for maintenance, etc.'),
|
||||||
|
'additional_camera_connected': fields.Boolean(required=False,
|
||||||
|
description='Indicates whether an additional camera is connected'),
|
||||||
|
'ip': fields.String(required=False, description='The recorder\'s IP address'),
|
||||||
|
'ip6': fields.String(required=False, description='The recorder\'s IP v6 address'),
|
||||||
|
'mac': fields.String(required=False, description='The recorder\'s MAC address'),
|
||||||
|
'network_name': fields.String(required=False, description='The recorder\'s network name'),
|
||||||
|
'ssh_port': fields.Integer(required=True, default=22, description='The recorder\'s SSH port number'),
|
||||||
|
'telnet_port': fields.Integer(required=True, default=23, description='The recorder\'s telnet port number'),
|
||||||
|
# 'use_telnet_instead_ssh': fields.Boolean(required=False, default=False,
|
||||||
|
# description='If this is set, telnet will be used instead of ssh. '
|
||||||
|
# 'This might require specific commands.'),
|
||||||
|
'recorder_model': fields.Nested(api_recorder.model('recorder_model',
|
||||||
|
{'id': fields.Integer(),
|
||||||
|
'name': fields.String(attribute="model_name", )}),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
skip_none=False,
|
||||||
|
description='Model of the recorder.'),
|
||||||
|
'room': fields.Nested(api_recorder.model('recorder_room',
|
||||||
|
{'id': fields.Integer(), 'name': fields.String(),
|
||||||
|
'number': fields.String(), 'alternate_name': fields.String()}),
|
||||||
|
r0equired=False,
|
||||||
|
allow_null=True,
|
||||||
|
skip_none=False,
|
||||||
|
description='Room in which the recorder is located.'),
|
||||||
|
'virtual_commands': fields.List(fields.Nested(api_recorder.model('recorder_virtual_commands',
|
||||||
|
{'id': fields.Integer(),
|
||||||
|
'name': fields.String()})))
|
||||||
|
})
|
||||||
|
|
||||||
|
recorder_command_model = api_recorder.model('Recorder Command', {
|
||||||
|
'id': fields.String(required=False, description='The recorder command\'s identifier'),
|
||||||
|
'name': fields.String(required=True, description='The recorder command\'s name'),
|
||||||
|
'alternative_name': fields.String(required=False, description='The recorder command\'s alternative name'),
|
||||||
|
'disabled': fields.Boolean(required=False, description='Indicates if the recorder command is disabled'),
|
||||||
|
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||||
|
'last_time_modified': fields.DateTime(required=False),
|
||||||
|
'description': fields.String(required=False, description='The recorder command\'s description'),
|
||||||
|
'parameters': fields.Raw(required=True, description='The recorder parameters'),
|
||||||
|
'recorder_model': fields.Nested(api_recorder.model('recorder_command_models',
|
||||||
|
{'id': fields.Integer(),
|
||||||
|
'name': fields.String(attribute="model_name", )})),
|
||||||
|
})
|
||||||
|
|
||||||
|
recorder_model_model = api_recorder.model('Recorder Model', {
|
||||||
|
'id': fields.String(required=False, description='The recorder model\'s identifier'),
|
||||||
|
'name': fields.String(attribute="model_name", required=True, description='The recorder model\'s name'),
|
||||||
|
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||||
|
'last_time_modified': fields.DateTime(required=False),
|
||||||
|
'notes': fields.String(required=False, description='The recorder model\'s notes'),
|
||||||
|
'requires_username': fields.Boolean(),
|
||||||
|
'requires_password': fields.Boolean(),
|
||||||
|
|
||||||
|
'recorders': fields.List(fields.Nested(api_recorder.model('recorder_model',
|
||||||
|
{'id': fields.Integer(),
|
||||||
|
'name': fields.String(attribute="model_name", ),
|
||||||
|
'network_name': fields.String(),
|
||||||
|
'ip': fields.String()})), required=False,
|
||||||
|
description='Model of the recorder.'),
|
||||||
|
'commands': fields.List(fields.Nested(recorder_command_model), attribute="recorder_commands")
|
||||||
|
})
|
||||||
|
|
||||||
|
state_model = api_state.model('Recorder State', {
|
||||||
|
'id': fields.String(required=False, description='The recorder model\'s identifier'),
|
||||||
|
'name': fields.String(description='The recorder model\'s name'),
|
||||||
|
'msg': fields.String(),
|
||||||
|
'state_ok': fields.Boolean(),
|
||||||
|
'time_stamp': fields.String(required=False),
|
||||||
|
'previous': fields.Nested(api_state.model('Previous Recorder State', {
|
||||||
|
'msg': fields.String(),
|
||||||
|
'state_ok': fields.Boolean(),
|
||||||
|
'time_stamp': fields.String()}, required=False, skip_none=True)
|
||||||
|
)
|
||||||
|
})
|
||||||
91
backend/api/permission_api.py
Normal file
91
backend/api/permission_api.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_restx import fields, Resource
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
from backend.api import api_permissions
|
||||||
|
from backend.models.user_model import Permission
|
||||||
|
|
||||||
|
permission_model = api_permissions.model('Permission', {
|
||||||
|
'id': fields.String(required=False, description='The permission\'s identifier'),
|
||||||
|
'name': fields.String(required=True, description='The permission\'s name'),
|
||||||
|
'description': fields.String(required=False, description='The permission\'s description'),
|
||||||
|
'groups': fields.List(fields.Nested(api_permissions.model('group_member',
|
||||||
|
{'id': fields.Integer(),
|
||||||
|
'name': fields.String(),
|
||||||
|
'description': fields.String()})),
|
||||||
|
required=False, description='Groups having the permission.'),
|
||||||
|
'access_control_entry': fields.Nested(api_permissions.model('group_member',
|
||||||
|
{'id': fields.Integer(),
|
||||||
|
'name': fields.String(),
|
||||||
|
'url': fields.String()}),
|
||||||
|
required=False, description="Access Control Entry"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_permissions.route('/<int:id>')
|
||||||
|
@api_permissions.response(404, 'permission not found')
|
||||||
|
@api_permissions.param('id', 'The permission identifier')
|
||||||
|
class PermissionResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_permissions.doc('get_permission')
|
||||||
|
@api_permissions.marshal_with(permission_model)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a user given its identifier"""
|
||||||
|
permission = Permission.get_by_id(id)
|
||||||
|
if permission is not None:
|
||||||
|
return permission
|
||||||
|
api_permissions.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_permissions.doc('delete_permission')
|
||||||
|
@api_permissions.response(204, 'permission deleted')
|
||||||
|
def delete(self, id):
|
||||||
|
"""Delete a permission given its identifier"""
|
||||||
|
permission = Permission.get_by_id(id)
|
||||||
|
if permission is not None:
|
||||||
|
permission.delete()
|
||||||
|
return '', 204
|
||||||
|
api_permissions.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_permissions.doc('update_permission')
|
||||||
|
@api_permissions.expect(permission_model)
|
||||||
|
@api_permissions.marshal_with(permission_model)
|
||||||
|
def put(self, id):
|
||||||
|
"""Update a task given its identifier"""
|
||||||
|
permission = Permission.get_by_id(id)
|
||||||
|
if permission is not None:
|
||||||
|
permission.name = api_permissions["name"]
|
||||||
|
db.session.commit()
|
||||||
|
return permission
|
||||||
|
api_permissions.abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@api_permissions.route('')
|
||||||
|
class PermissionList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_permissions.doc('permissions')
|
||||||
|
@api_permissions.marshal_list_with(permission_model)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
List all permissions
|
||||||
|
:return: permissions
|
||||||
|
"""
|
||||||
|
return Permission.get_all()
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_permissions.doc('create_permission')
|
||||||
|
@api_permissions.expect(permission_model)
|
||||||
|
@api_permissions.marshal_with(permission_model, code=201)
|
||||||
|
def post(self):
|
||||||
|
permission = Permission(**api_permissions.payload)
|
||||||
|
db.session.add(permission)
|
||||||
|
db.session.commit()
|
||||||
|
return permission
|
||||||
221
backend/api/recorder_api.py
Normal file
221
backend/api/recorder_api.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to recorders through the API.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_restx import fields, Resource, inputs
|
||||||
|
|
||||||
|
from backend import db, app, LrcException, Config
|
||||||
|
from backend.api import api_recorder
|
||||||
|
from backend.api.models import recorder_model, recorder_model_model, recorder_command_model
|
||||||
|
from backend.auth.utils import requires_permission_level
|
||||||
|
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
|
||||||
|
from backend.models.room_model import Room
|
||||||
|
import backend.recorder_adapters as r_a
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.api.recorder")
|
||||||
|
|
||||||
|
# ==
|
||||||
|
|
||||||
|
@api_recorder.route('/<int:id>')
|
||||||
|
@api_recorder.response(404, 'Recorder not found')
|
||||||
|
@api_recorder.param('id', 'The recorder identifier')
|
||||||
|
class RecorderResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECODER_SHOW)
|
||||||
|
@api_recorder.doc('get_recorder')
|
||||||
|
@api_recorder.marshal_with(recorder_model, skip_none=False)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a recorder given its identifier"""
|
||||||
|
recorder = Recorder.query.get(id)
|
||||||
|
if recorder is not None:
|
||||||
|
return recorder
|
||||||
|
api_recorder.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECORDER_DELETE)
|
||||||
|
@api_recorder.doc('delete_todo')
|
||||||
|
@api_recorder.response(204, 'Todo deleted')
|
||||||
|
def delete(self, id):
|
||||||
|
"""Delete a recorder given its identifier"""
|
||||||
|
recorder = Recorder.query.get(id)
|
||||||
|
if recorder is not None:
|
||||||
|
db.session.delete(recorder)
|
||||||
|
db.session.commit()
|
||||||
|
return '', 204
|
||||||
|
api_recorder.abort(404)
|
||||||
|
|
||||||
|
recorder_update_parser = api_recorder.parser()
|
||||||
|
recorder_update_parser.add_argument('name', type=str, required=False, nullable=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('network_name', type=inputs.regex(inputs.netloc_regex), required=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('ip', type=inputs.ipv4, required=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('ip6', type=inputs.ipv6, required=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('ssh_port', type=inputs.int_range(0,65535), required=False, default=22, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('telnet_port', type=inputs.int_range(0,65535), required=False, default=23, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('room_id', type=int, required=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('offline', type=inputs.boolean, required=False, default=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('locked', type=inputs.boolean, required=False, default=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('lock_message', type=str, required=False, nullable=True, default=None,
|
||||||
|
store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('model_id', type=int, required=False, store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('description', type=str, required=False, nullable=True, default=None,
|
||||||
|
store_missing=False)
|
||||||
|
recorder_update_parser.add_argument('virtual_command_ids', action='split', nullable=True, default=[],
|
||||||
|
required=False, store_missing=False)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECORDER_EDIT)
|
||||||
|
@api_recorder.doc('update_recorder')
|
||||||
|
@api_recorder.expect(recorder_model)
|
||||||
|
def put(self, id):
|
||||||
|
"""Update a recorder given its identifier"""
|
||||||
|
args = self.recorder_update_parser.parse_args(strict=True)
|
||||||
|
args['last_time_modified'] = datetime.utcnow()
|
||||||
|
pprint(args)
|
||||||
|
|
||||||
|
num_rows_matched = Recorder.query.filter_by(id=id).update(args)
|
||||||
|
print(num_rows_matched)
|
||||||
|
|
||||||
|
if num_rows_matched < 1:
|
||||||
|
api_recorder.abort(404)
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@api_recorder.route('')
|
||||||
|
class RecorderList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECORDERS_LIST)
|
||||||
|
@api_recorder.doc('recorders')
|
||||||
|
@api_recorder.marshal_list_with(recorder_model, skip_none=False)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
List all recorders
|
||||||
|
:return: recorders
|
||||||
|
"""
|
||||||
|
return Recorder.get_all()
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECODER_NEW)
|
||||||
|
@api_recorder.doc('create_recorder')
|
||||||
|
@api_recorder.expect(recorder_model)
|
||||||
|
@api_recorder.marshal_with(recorder_model, skip_none=False, code=201)
|
||||||
|
def post(self):
|
||||||
|
if "room_id" in api_recorder.payload:
|
||||||
|
if api_recorder.payload["room_id"] is None:
|
||||||
|
api_recorder.payload["room"] = None
|
||||||
|
else:
|
||||||
|
room = Room.query.get(api_recorder.payload["room_id"])
|
||||||
|
if room is not None:
|
||||||
|
api_recorder.payload["room"] = room
|
||||||
|
else:
|
||||||
|
return "specified room (id: {}) does not exist!".format(api_recorder.payload["room_id"]), 404
|
||||||
|
if "recorder_model_id" in api_recorder.payload:
|
||||||
|
if api_recorder.payload["recorder_model_id"] is None:
|
||||||
|
api_recorder.payload["recorder_model"] = None
|
||||||
|
else:
|
||||||
|
rec_model = RecorderModel.query.get(api_recorder.payload["recorder_model_id"])
|
||||||
|
if rec_model is not None:
|
||||||
|
api_recorder.payload["recorder_model"] = rec_model
|
||||||
|
else:
|
||||||
|
return "specified recorder model (id: {}) does not exist!".format(
|
||||||
|
api_recorder.payload["recorder_model_id"]), 404
|
||||||
|
try:
|
||||||
|
recorder = Recorder(**api_recorder.payload)
|
||||||
|
db.session.add(recorder)
|
||||||
|
db.session.commit()
|
||||||
|
return recorder
|
||||||
|
except LrcException as e:
|
||||||
|
logger.error(e)
|
||||||
|
return api_recorder.abort(400, message=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@api_recorder.route('/model/<int:id>')
|
||||||
|
@api_recorder.response(404, 'Recorder Model not found')
|
||||||
|
@api_recorder.param('id', 'The recorder model identifier')
|
||||||
|
class RecorderModelResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_recorder.doc('get_recorder_model')
|
||||||
|
@api_recorder.marshal_with(recorder_model_model)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a recorder model given its identifier"""
|
||||||
|
recorder_model = RecorderModel.query.get(id)
|
||||||
|
if recorder_model is not None:
|
||||||
|
return recorder_model
|
||||||
|
api_recorder.abort(404)
|
||||||
|
|
||||||
|
recorder_model_parser = api_recorder.parser()
|
||||||
|
recorder_model_parser.add_argument('notes', type=str, required=True)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_recorder.doc('update_recorder_model')
|
||||||
|
@api_recorder.expect(recorder_model_parser)
|
||||||
|
@api_recorder.marshal_with(recorder_model_model)
|
||||||
|
def put(self, id):
|
||||||
|
"""Update a recorder_model given its identifier"""
|
||||||
|
num_rows_matched = RecorderModel.query.filter_by(id=id).update(api_recorder.payload)
|
||||||
|
if num_rows_matched < 1:
|
||||||
|
api_recorder.abort(404)
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@api_recorder.route('/model')
|
||||||
|
class RecorderModelList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECODER_MODELS_LIST)
|
||||||
|
@api_recorder.doc('recorders')
|
||||||
|
@api_recorder.marshal_list_with(recorder_model_model)
|
||||||
|
def get(self):
|
||||||
|
return RecorderModel.get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@api_recorder.route('/command/<int:id>')
|
||||||
|
@api_recorder.response(404, 'Recorder Command not found')
|
||||||
|
@api_recorder.param('id', 'The recorder command identifier')
|
||||||
|
class RecorderCommandResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECORDER_COMMAND_SHOW)
|
||||||
|
@api_recorder.doc('get_recorder_command')
|
||||||
|
@api_recorder.marshal_with(recorder_command_model)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a recorder command given its identifier"""
|
||||||
|
recorder_command = RecorderCommand.query.get(id)
|
||||||
|
if recorder_command is not None:
|
||||||
|
return recorder_command
|
||||||
|
api_recorder.abort(404)
|
||||||
|
|
||||||
|
recorder_command_model_parser = api_recorder.parser()
|
||||||
|
recorder_command_model_parser.add_argument('description', type=str, required=False)
|
||||||
|
recorder_command_model_parser.add_argument('alternative_name', type=str, required=False)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECORDER_COMMAND_EDIT)
|
||||||
|
@api_recorder.doc('update_recorder_command')
|
||||||
|
@api_recorder.expect(recorder_command_model_parser)
|
||||||
|
@api_recorder.marshal_with(recorder_command_model)
|
||||||
|
def put(self, id):
|
||||||
|
"""Update a recorder command given its identifier"""
|
||||||
|
num_rows_matched = RecorderCommand.query.filter_by(id=id).update(api_recorder.payload)
|
||||||
|
if num_rows_matched < 1:
|
||||||
|
api_recorder.abort(404)
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@api_recorder.route('/command')
|
||||||
|
class RecorderCommandList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.RECORDER_COMMANDS_LIST)
|
||||||
|
@api_recorder.doc('recorder_commands')
|
||||||
|
@api_recorder.marshal_list_with(recorder_command_model)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
List all recorders commands
|
||||||
|
:return: recorder commands
|
||||||
|
"""
|
||||||
|
return RecorderCommand.get_all()
|
||||||
131
backend/api/room_api.py
Normal file
131
backend/api/room_api.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_restx import fields, Resource
|
||||||
|
from sqlalchemy import exc
|
||||||
|
|
||||||
|
from backend import db, app, LrcException
|
||||||
|
from backend.api import api_room
|
||||||
|
from backend.models.room_model import Room
|
||||||
|
from backend.models.recorder_model import Recorder
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.api.room")
|
||||||
|
|
||||||
|
room_model = api_room.model('Room', {
|
||||||
|
'id': fields.String(required=False, description='The room\'s identifier'),
|
||||||
|
'created_at': fields.DateTime(required=False, description='Creation date of the room info'),
|
||||||
|
'name': fields.String(required=True, description='The room\'s name'),
|
||||||
|
'alternate_name': fields.String(required=False, description='The room\'s alternate name'),
|
||||||
|
'comment': fields.String(required=False, description='The room\'s comment'),
|
||||||
|
'number': fields.String(required=True, description='The room\'s number'),
|
||||||
|
'building_name': fields.String(required=False, description='The building\'s name'),
|
||||||
|
'building_number': fields.String(required=False, description='The building\'s number'),
|
||||||
|
'recorder': fields.Nested(api_room.model('room_recorder',
|
||||||
|
{'id': fields.Integer(), 'name': fields.String(),
|
||||||
|
'ip': fields.String(), 'network_name': fields.String()}),
|
||||||
|
allow_null=True,
|
||||||
|
skip_none=False,
|
||||||
|
required=False,
|
||||||
|
description='Room recorder.'),
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_room.route('/<int:id>')
|
||||||
|
@api_room.response(404, 'Room not found')
|
||||||
|
@api_room.param('id', 'The room identifier')
|
||||||
|
class RoomResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_room.doc('get_room')
|
||||||
|
@api_room.marshal_with(room_model, skip_none=False)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a user given its identifier"""
|
||||||
|
room = Room.query.get(id)
|
||||||
|
if room is not None:
|
||||||
|
return room
|
||||||
|
api_room.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_room.doc('delete_todo')
|
||||||
|
@api_room.response(204, 'Todo deleted')
|
||||||
|
def delete(self, id):
|
||||||
|
'''Delete a task given its identifier'''
|
||||||
|
room = Room.query.get(id)
|
||||||
|
if room is not None:
|
||||||
|
db.session.delete(room)
|
||||||
|
db.session.commit()
|
||||||
|
return '', 204
|
||||||
|
api_room.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_room.doc('update_room')
|
||||||
|
@api_room.expect(room_model)
|
||||||
|
def put(self, id):
|
||||||
|
app.logger.debug(api_room.payload)
|
||||||
|
'''Update a task given its identifier'''
|
||||||
|
if "recorder_id" in api_room.payload:
|
||||||
|
if api_room.payload["recorder_id"] is None:
|
||||||
|
api_room.payload["recorder"] = None
|
||||||
|
else:
|
||||||
|
recorder = Recorder.query.get(api_room.payload["recorder_id"])
|
||||||
|
if recorder is not None:
|
||||||
|
api_room.payload["recorder"] = recorder
|
||||||
|
else:
|
||||||
|
return "specified recorder (id: {}) does not exist!".format(api_room.payload["recorder_id"]), 404
|
||||||
|
room = Room.query.get(id)
|
||||||
|
if room is not None:
|
||||||
|
room.recorder = api_room.payload["recorder"]
|
||||||
|
else:
|
||||||
|
num_rows_matched = Room.query.filter_by(id=id).update(api_room.payload)
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
api_room.abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@api_room.route('')
|
||||||
|
class RoomList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_room.doc('rooms')
|
||||||
|
@api_room.marshal_list_with(room_model, skip_none=False)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
List all rooms
|
||||||
|
:return: rooms
|
||||||
|
"""
|
||||||
|
return Room.get_all()
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_room.doc('create_room')
|
||||||
|
@api_room.expect(room_model)
|
||||||
|
@api_room.marshal_with(room_model, skip_none=False, code=201)
|
||||||
|
def post(self):
|
||||||
|
if "recorder_id" in api_room.payload:
|
||||||
|
if api_room.payload["recorder_id"] is None:
|
||||||
|
api_room.payload["recorder"] = None
|
||||||
|
else:
|
||||||
|
recorder = Recorder.query.get(api_room.payload["recorder_id"])
|
||||||
|
if recorder is not None:
|
||||||
|
api_room.payload["recorder"] = recorder
|
||||||
|
else:
|
||||||
|
return "specified recorder (id: {}) does not exist!".format(api_room.payload["recorder_id"]), 404
|
||||||
|
del api_room.payload["recorder_id"]
|
||||||
|
try:
|
||||||
|
room = Room(**api_room.payload)
|
||||||
|
db.session.add(room)
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
return room
|
||||||
|
except exc.IntegrityError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return str(e.detail), 400
|
||||||
|
except LrcException as e:
|
||||||
|
logger.error(e)
|
||||||
|
return api_room.abort(400, message=str(e))
|
||||||
62
backend/api/state_api.py
Normal file
62
backend/api/state_api.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to object states through the API.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_restx import fields, Resource, inputs
|
||||||
|
|
||||||
|
from backend import db, app
|
||||||
|
from backend.api import api_state
|
||||||
|
from backend.api.models import recorder_model, recorder_model_model, recorder_command_model, state_model
|
||||||
|
from backend.cron import async_cron_recorder_checker, async_permanent_cron_recorder_checker
|
||||||
|
from backend.cron.cron_state_checker import StateChecker
|
||||||
|
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
|
||||||
|
from backend.models.room_model import Room
|
||||||
|
import backend.recorder_adapters as r_a
|
||||||
|
|
||||||
|
# ==
|
||||||
|
from backend.tools.recorder_state_checker import get_recorder_adapter, check_capture_agent_state, check_stream_sanity
|
||||||
|
|
||||||
|
|
||||||
|
@api_state.route('/recorder/<int:id>')
|
||||||
|
@api_state.response(404, 'Recorder not found')
|
||||||
|
@api_state.param('id', 'The recorder identifier')
|
||||||
|
class RecorderStateResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_state.doc('get_recorder_state')
|
||||||
|
@api_state.marshal_with(state_model, skip_none=False)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a recorder given its identifier"""
|
||||||
|
recorder: Recorder = Recorder.query.get(id)
|
||||||
|
if recorder is None:
|
||||||
|
api_state.abort(404)
|
||||||
|
|
||||||
|
# current_states_by_checker = async_cron_recorder_checker.get_current_state()
|
||||||
|
state = async_permanent_cron_recorder_checker.get_current_state_for_recorder_id(recorder.id)
|
||||||
|
if state is None:
|
||||||
|
state_checker = StateChecker([check_capture_agent_state], Recorder)
|
||||||
|
state_checker.add_object_to_state_check(recorder)
|
||||||
|
state_checker.check_object_state()
|
||||||
|
state = state_checker.get_current_state_for_recorder_id(recorder.id)
|
||||||
|
print("lalala")
|
||||||
|
print(state)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
@api_state.route('/recorder')
|
||||||
|
class RecorderStateList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_state.doc('get_recorders_states')
|
||||||
|
@api_state.marshal_list_with(state_model, skip_none=True)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get state of all recorders
|
||||||
|
:return: state
|
||||||
|
"""
|
||||||
|
rec_states = async_permanent_cron_recorder_checker.get_current_state()
|
||||||
|
print(rec_states)
|
||||||
|
res = [{**{'name': rec_state_name}, **rec_states[rec_state_name]} for rec_state_name in rec_states]
|
||||||
|
return res
|
||||||
148
backend/api/user_api.py
Normal file
148
backend/api/user_api.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from flask_jwt_extended import get_jwt_identity, jwt_required, current_user
|
||||||
|
from flask_restx import Resource, fields, inputs, abort
|
||||||
|
|
||||||
|
from backend import db, app, jwt_auth
|
||||||
|
from backend.api import api_user
|
||||||
|
from backend.api.models import user_model, recorder_model, generic_id_parser
|
||||||
|
from backend.auth.utils import requires_permission_level
|
||||||
|
from backend.models import Recorder, Config
|
||||||
|
from backend.models.user_model import User, Group
|
||||||
|
|
||||||
|
|
||||||
|
user_update_parser = api_user.parser()
|
||||||
|
user_update_parser.add_argument('email', type=inputs.email(), required=False, nullable=False, store_missing=False)
|
||||||
|
user_update_parser.add_argument('nickname', type=str, required=False, store_missing=False)
|
||||||
|
user_update_parser.add_argument('first_name', type=str, required=False, store_missing=False)
|
||||||
|
user_update_parser.add_argument('last_name', type=str, required=False, store_missing=False)
|
||||||
|
|
||||||
|
|
||||||
|
@api_user.route('/profile')
|
||||||
|
class Profile(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_user.marshal_with(user_model)
|
||||||
|
def get(self):
|
||||||
|
"""Get infos about logged in user."""
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
app.logger.info(current_user_id)
|
||||||
|
return User.get_by_identifier(current_user_id)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_user.expect(user_update_parser)
|
||||||
|
def put(self):
|
||||||
|
args = user_update_parser.parse_args()
|
||||||
|
args['last_time_modified'] = datetime.utcnow()
|
||||||
|
pprint(args)
|
||||||
|
print(current_user)
|
||||||
|
num_rows_matched = User.query.filter_by(id=current_user.id).update(args)
|
||||||
|
print(num_rows_matched)
|
||||||
|
|
||||||
|
if num_rows_matched < 1:
|
||||||
|
api_user.abort("Nothing has been updated!")
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@api_user.route('/profile/favorite_recorders')
|
||||||
|
class UserFavoriteRecorders(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_user.marshal_list_with(recorder_model)
|
||||||
|
def get(self):
|
||||||
|
try:
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
return User.get_by_identifier(current_user_id).favorite_recorders
|
||||||
|
except AttributeError:
|
||||||
|
abort(404, "User not found!")
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_user.expect(generic_id_parser)
|
||||||
|
@api_user.marshal_list_with(recorder_model)
|
||||||
|
def put(self):
|
||||||
|
try:
|
||||||
|
args = generic_id_parser.parse_args()
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
user = User.get_by_identifier(current_user_id)
|
||||||
|
print(args)
|
||||||
|
recorder = Recorder.get_by_identifier(args["id"])
|
||||||
|
print(recorder)
|
||||||
|
if recorder is None:
|
||||||
|
abort(404, "(Specified [id: {}]) recorder not found!".format(args["id"]))
|
||||||
|
user.favorite_recorders.append(recorder)
|
||||||
|
db.session.commit()
|
||||||
|
return user.favorite_recorders
|
||||||
|
except AttributeError:
|
||||||
|
abort(404, "User not found!")
|
||||||
|
|
||||||
|
|
||||||
|
@api_user.route('')
|
||||||
|
class UserList(Resource):
|
||||||
|
"""
|
||||||
|
This is a test class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# @jwt_auth.login_required
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.USERS_LIST)
|
||||||
|
@api_user.doc('users')
|
||||||
|
@api_user.marshal_list_with(user_model)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
returns all users
|
||||||
|
:return: all users
|
||||||
|
"""
|
||||||
|
current_user = get_jwt_identity()
|
||||||
|
app.logger.info(current_user)
|
||||||
|
return User.get_all()
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@requires_permission_level(Config.Permissions.USER_CREATE)
|
||||||
|
@api_user.doc('create_group')
|
||||||
|
@api_user.expect(user_model)
|
||||||
|
@api_user.marshal_with(user_model, code=201)
|
||||||
|
def post(self):
|
||||||
|
user = User(**api_user.payload)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@api_user.route('/<id>')
|
||||||
|
@api_user.param('id', 'The user identifier')
|
||||||
|
@api_user.response(404, 'User not found')
|
||||||
|
class UserResource(Resource):
|
||||||
|
@jwt_auth.login_required
|
||||||
|
@requires_permission_level(Config.Permissions.USER_SHOW)
|
||||||
|
@api_user.doc('get_user')
|
||||||
|
@api_user.marshal_with(user_model)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a user given its identifier"""
|
||||||
|
user = User.get_by_id(id)
|
||||||
|
if user is not None:
|
||||||
|
return user
|
||||||
|
api_user.abort(404)
|
||||||
|
|
||||||
|
@jwt_auth.login_required
|
||||||
|
@requires_permission_level(Config.Permissions.USER_DELETE)
|
||||||
|
@api_user.doc('delete_user')
|
||||||
|
def delete(self, id):
|
||||||
|
"""Fetch a user given its identifier"""
|
||||||
|
user = User.get_by_id(id)
|
||||||
|
if user is not None:
|
||||||
|
if str(user.role) == str(Config.Roles.ADMIN):
|
||||||
|
print("role deletion forbidden")
|
||||||
|
return api_user.abort(403, message="It is not allowed to delete role admin users!")
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
api_user.abort(404)
|
||||||
|
|
||||||
|
# api_user.add_resource(UserResource, '/')
|
||||||
150
backend/api/virtual_command_api.py
Normal file
150
backend/api/virtual_command_api.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This module provides functions related to authentication through the API.
|
||||||
|
For example: listing of available auth providers or registration of users.
|
||||||
|
|
||||||
|
Login through API does not start a new session, but instead returns JWT.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
import pkgutil
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from flask_restx import fields, Resource
|
||||||
|
|
||||||
|
from backend import db, app
|
||||||
|
from backend.api import api_virtual_command, recorder_command_model
|
||||||
|
from backend.models import VirtualCommand
|
||||||
|
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
|
||||||
|
from backend.models.room_model import Room
|
||||||
|
import backend.recorder_adapters as r_a
|
||||||
|
|
||||||
|
virtual_command_model = api_virtual_command.model('VirtualCommand', {
|
||||||
|
'id': fields.String(required=False, description='The recorder\'s identifier'),
|
||||||
|
'created_at': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||||
|
'name': fields.String(min_length=3, required=True, description='The recorder\'s name'),
|
||||||
|
'description': fields.String(required=False, description='The recorder\'s description'),
|
||||||
|
|
||||||
|
'parent_virtual_command': fields.Nested(api_virtual_command.model('VirtualCommandParent',
|
||||||
|
{
|
||||||
|
'id': fields.String(required=False,
|
||||||
|
description='The recorder\'s identifier'),
|
||||||
|
'name': fields.String(min_length=3,
|
||||||
|
required=True,
|
||||||
|
description='The recorder\'s name'),
|
||||||
|
},
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
skip_none=False,
|
||||||
|
description='Parent virtual command.')),
|
||||||
|
|
||||||
|
'recorder_commands': fields.List(fields.Nested(recorder_command_model)),
|
||||||
|
|
||||||
|
'command_default_params': fields.List(fields.Raw()),
|
||||||
|
'room': fields.Nested(api_virtual_command.model('recorder_room',
|
||||||
|
{'id': fields.Integer(), 'name': fields.String(),
|
||||||
|
'number': fields.String(), 'alternate_name': fields.String()}),
|
||||||
|
r0equired=False,
|
||||||
|
allow_null=True,
|
||||||
|
skip_none=False,
|
||||||
|
description='Room in which the recorder is located.')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ==
|
||||||
|
|
||||||
|
@api_virtual_command.route('/<int:id>')
|
||||||
|
@api_virtual_command.response(404, 'Recorder not found')
|
||||||
|
@api_virtual_command.param('id', 'The recorder identifier')
|
||||||
|
class VirtualCommandResource(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_virtual_command.doc('get_recorder')
|
||||||
|
@api_virtual_command.marshal_with(virtual_command_model, skip_none=False)
|
||||||
|
def get(self, id):
|
||||||
|
"""Fetch a recorder given its identifier"""
|
||||||
|
command = VirtualCommand.query.get(id)
|
||||||
|
if command is not None:
|
||||||
|
return command
|
||||||
|
api_virtual_command.abort(404)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_virtual_command.doc('delete_todo')
|
||||||
|
@api_virtual_command.response(204, 'Todo deleted')
|
||||||
|
def delete(self, id):
|
||||||
|
"""Delete a recorder given its identifier"""
|
||||||
|
recorder = Recorder.query.get(id)
|
||||||
|
if recorder is not None:
|
||||||
|
db.session.delete(recorder)
|
||||||
|
db.session.commit()
|
||||||
|
return '', 204
|
||||||
|
api_virtual_command.abort(404)
|
||||||
|
|
||||||
|
virtual_command_model_parser = api_virtual_command.parser()
|
||||||
|
virtual_command_model_parser.add_argument('notes', type=str, required=True)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_virtual_command.doc('update_recorder')
|
||||||
|
@api_virtual_command.expect(virtual_command_model_parser)
|
||||||
|
def put(self, id):
|
||||||
|
"""Update a recorder given its identifier"""
|
||||||
|
num_rows_matched = Recorder.query.filter_by(id=id).update(api_virtual_command.payload)
|
||||||
|
if num_rows_matched < 1:
|
||||||
|
api_virtual_command.abort(404)
|
||||||
|
db.session.commit()
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
@api_virtual_command.route('')
|
||||||
|
class VirtualCommandList(Resource):
|
||||||
|
@jwt_required
|
||||||
|
@api_virtual_command.doc('recorders')
|
||||||
|
@api_virtual_command.marshal_list_with(virtual_command_model, skip_none=False)
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
List all recorders
|
||||||
|
:return: recorders
|
||||||
|
"""
|
||||||
|
return VirtualCommand.get_all()
|
||||||
|
|
||||||
|
virtual_command_model_parser = api_virtual_command.parser()
|
||||||
|
virtual_command_model_parser.add_argument('notes', type=str, required=True)
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
@api_virtual_command.doc('create_recorder')
|
||||||
|
@api_virtual_command.expect(virtual_command_model_parser)
|
||||||
|
@api_virtual_command.marshal_with(virtual_command_model, skip_none=False, code=201)
|
||||||
|
def post(self):
|
||||||
|
pprint(api_virtual_command.payload)
|
||||||
|
room_id = api_virtual_command.payload.pop('recorder_id', None)
|
||||||
|
if room_id is None:
|
||||||
|
api_virtual_command.payload["room"] = None
|
||||||
|
else:
|
||||||
|
room = Room.query.get(room_id)
|
||||||
|
if room is not None:
|
||||||
|
api_virtual_command.payload["room"] = room
|
||||||
|
else:
|
||||||
|
return "specified v-command (id: {}) does not exist!".format(api_virtual_command.payload["room_id"]), 404
|
||||||
|
recorder_model_id = api_virtual_command.payload.pop('recorder_model_id', None)
|
||||||
|
if recorder_model_id is None:
|
||||||
|
api_virtual_command.payload["recorder_model"] = None
|
||||||
|
else:
|
||||||
|
rec_model = RecorderModel.query.get(recorder_model_id)
|
||||||
|
if rec_model is not None:
|
||||||
|
api_virtual_command.payload["recorder_model"] = rec_model
|
||||||
|
else:
|
||||||
|
return "specified recorder model (id: {}) does not exist!".format(
|
||||||
|
api_virtual_command.payload["recorder_model_id"]), 404
|
||||||
|
recorder_id = api_virtual_command.payload.pop('recorder_id', None)
|
||||||
|
if recorder_id is None:
|
||||||
|
api_virtual_command.payload["recorder"] = None
|
||||||
|
else:
|
||||||
|
recorder = Recorder.query.get(recorder_id)
|
||||||
|
if recorder is not None:
|
||||||
|
api_virtual_command.payload["recorder"] = recorder
|
||||||
|
else:
|
||||||
|
return "specified v-command (id: {}) does not exist!".format(
|
||||||
|
api_virtual_command.payload["recorder_id"]), 404
|
||||||
|
virtual_command = VirtualCommand(**api_virtual_command.payload)
|
||||||
|
db.session.add(virtual_command)
|
||||||
|
db.session.commit()
|
||||||
|
return virtual_command
|
||||||
BIN
backend/app.db_debug
Normal file
BIN
backend/app.db_debug
Normal file
Binary file not shown.
BIN
backend/app.db_old
Normal file
BIN
backend/app.db_old
Normal file
Binary file not shown.
BIN
backend/app.db_test
Normal file
BIN
backend/app.db_test
Normal file
Binary file not shown.
78
backend/auth/__init__.py
Normal file
78
backend/auth/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
Base module for auth aspects.
|
||||||
|
|
||||||
|
Also this module contains mainly code for login through HTML pages served by the backend.
|
||||||
|
If frontend pages are build by frontend code (JS, etc.) authentication should consider using api functions.
|
||||||
|
(For more info, see api.auth_api.py.)
|
||||||
|
|
||||||
|
This code uses login_user and logout user (to start and end sessions) ... API code returns JWTs.
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, jsonify, url_for
|
||||||
|
from flask_login import logout_user, LoginManager
|
||||||
|
from werkzeug.routing import BuildError
|
||||||
|
|
||||||
|
from backend import jwt_extended
|
||||||
|
from backend.models import BlacklistToken, User
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__, url_prefix='/auth', template_folder='templates')
|
||||||
|
|
||||||
|
from backend.auth.config import AUTH_PROVIDERS, DEFAULT_FRONTEND_PROVIDER
|
||||||
|
from backend.auth.oidc_config import OIDC_PROVIDERS
|
||||||
|
|
||||||
|
from backend.auth.oidc import oidc_auth
|
||||||
|
|
||||||
|
from .basic_auth import *
|
||||||
|
|
||||||
|
|
||||||
|
def auth_decorator(): # custom decorator
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
try:
|
||||||
|
prov = AUTH_PROVIDERS[DEFAULT_FRONTEND_PROVIDER]
|
||||||
|
except KeyError:
|
||||||
|
return "No known default provider specified!"
|
||||||
|
url = prov["url"]
|
||||||
|
try:
|
||||||
|
url = url_for(prov["url"], next=request.endpoint)
|
||||||
|
except BuildError as e:
|
||||||
|
pass
|
||||||
|
#logger.log("Can't create endpoint for '{}' (specified provider: {}).".format(e.endpoint, DEFAULT_PROVIDER))
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/login_select', methods=['GET'])
|
||||||
|
def login_select():
|
||||||
|
return render_template('login_select.html', providers=AUTH_PROVIDERS)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/logout', methods=('GET', ))
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_extended.user_claims_loader
|
||||||
|
def add_claims_to_access_token(user):
|
||||||
|
if isinstance(user, str):
|
||||||
|
return {}
|
||||||
|
return {'role': user.role, 'groups': [g.to_dict() for g in user.groups]}
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_extended.user_identity_loader
|
||||||
|
def user_identity_loader(user):
|
||||||
|
return user.email
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_extended.user_loader_callback_loader
|
||||||
|
def user_loader_callback(identity):
|
||||||
|
print("### user_loader_callback_loader")
|
||||||
|
return User.get_by_identifier(identity)
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_extended.token_in_blacklist_loader
|
||||||
|
def check_if_token_in_blacklist(decrypted_token):
|
||||||
|
jti = decrypted_token['jti']
|
||||||
|
return BlacklistToken.get_by_token(jti) is not None
|
||||||
22
backend/auth/basic_auth.py
Normal file
22
backend/auth/basic_auth.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Route for handling the login page logic
|
||||||
|
from flask import request, redirect, render_template
|
||||||
|
from flask_login import login_user
|
||||||
|
|
||||||
|
from backend.auth import auth_bp
|
||||||
|
from backend.models.user_model import User
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/base_login', methods=['GET', 'POST'])
|
||||||
|
def base_login():
|
||||||
|
error = None
|
||||||
|
if request.method == 'POST':
|
||||||
|
user = User.authenticate(email=request.form['email'], password=request.form['password'])
|
||||||
|
if user is None:
|
||||||
|
error = 'Invalid Credentials. Please try again.'
|
||||||
|
else:
|
||||||
|
login_user(user)
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
return render_template('login.html', error=error)
|
||||||
|
|
||||||
|
|
||||||
29
backend/auth/config.py
Normal file
29
backend/auth/config.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
AUTH_PROVIDERS: Dict[str, Dict[str, str]] = {
|
||||||
|
"KIT OIDC":
|
||||||
|
{
|
||||||
|
"type": "oidc",
|
||||||
|
"url": "auth_api.oidc"
|
||||||
|
},
|
||||||
|
"Base Login":
|
||||||
|
{
|
||||||
|
"type": "login_form",
|
||||||
|
"url": "auth.base_login"
|
||||||
|
},
|
||||||
|
"KIT OIDC (API)":
|
||||||
|
{
|
||||||
|
"type": "api_oidc",
|
||||||
|
"url": "auth_api.oidc"
|
||||||
|
},
|
||||||
|
"User-Password (API)":
|
||||||
|
{
|
||||||
|
"type": "api_login_form",
|
||||||
|
"url": "auth_api.login"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#DEFAULT_PROVIDER: str = "Base Login"
|
||||||
|
DEFAULT_PROVIDER: str = "KIT OIDC (API)"
|
||||||
|
|
||||||
|
DEFAULT_FRONTEND_PROVIDER: str = "Base Login"
|
||||||
93
backend/auth/oidc.py
Normal file
93
backend/auth/oidc.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
OIDC login auth module
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from flask import jsonify, redirect, url_for
|
||||||
|
from flask_login import login_user
|
||||||
|
from flask_pyoidc.flask_pyoidc import OIDCAuthentication
|
||||||
|
from flask_pyoidc.user_session import UserSession
|
||||||
|
|
||||||
|
from backend import app, db
|
||||||
|
from backend.models.user_model import User
|
||||||
|
from . import auth_bp
|
||||||
|
from .oidc_config import PROVIDER_NAME, OIDC_PROVIDERS
|
||||||
|
|
||||||
|
OIDCAuthentication.oidc_auth_orig = OIDCAuthentication.oidc_auth
|
||||||
|
OIDCAuthentication.oidc_logout_orig = OIDCAuthentication.oidc_logout
|
||||||
|
|
||||||
|
'''
|
||||||
|
def oidc_auth_default_provider(self):
|
||||||
|
"""monkey patch oidc_auth"""
|
||||||
|
return self.oidc_auth_orig(PROVIDER_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def oidc_logout_default_provider(self):
|
||||||
|
"""monkey patch oidc_logout"""
|
||||||
|
return self.oidc_logout_orig(PROVIDER_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
OIDCAuthentication.oidc_auth = oidc_auth_default_provider
|
||||||
|
OIDCAuthentication.oidc_logout = oidc_logout_default_provider
|
||||||
|
'''
|
||||||
|
|
||||||
|
oidc_auth = OIDCAuthentication(OIDC_PROVIDERS)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_retrieve_user_from_userinfo(userinfo):
|
||||||
|
"""Updates and returns ar creates a user from userinfo (part of OIDC token)."""
|
||||||
|
try:
|
||||||
|
email = userinfo["email"]
|
||||||
|
except KeyError:
|
||||||
|
app.logger.error("email is missing in OIDC userinfo! Can't create user!")
|
||||||
|
return None
|
||||||
|
user = User.get_by_identifier(email)
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
app.logger.info("user found")
|
||||||
|
app.logger.debug(f"user found: {email}")
|
||||||
|
user.last_seen = datetime.utcnow()
|
||||||
|
# TODO: update user!
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
user = User(email=email, first_name=userinfo.get("given_name", ""),
|
||||||
|
last_name=userinfo.get("family_name", ""))
|
||||||
|
|
||||||
|
app.logger.info(f"creating new user: {email}")
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/oidc', methods=['GET'])
|
||||||
|
@oidc_auth.oidc_auth(provider_name=PROVIDER_NAME)
|
||||||
|
def oidc():
|
||||||
|
user_session = UserSession(flask.session)
|
||||||
|
app.logger.info(user_session.userinfo)
|
||||||
|
user = create_or_retrieve_user_from_userinfo(user_session.userinfo)
|
||||||
|
if user is None:
|
||||||
|
return ''
|
||||||
|
login_user(user)
|
||||||
|
app.logger.info(f"logged in user: {str(user)}")
|
||||||
|
app.logger.debug(f"id token: {str(user_session.id_token)}")
|
||||||
|
return jsonify(id_token=user_session.id_token,
|
||||||
|
access_token=flask.session['access_token'],
|
||||||
|
userinfo=user_session.userinfo)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/oidc_logout', methods=['GET'])
|
||||||
|
@oidc_auth.oidc_logout
|
||||||
|
def oidc_logout():
|
||||||
|
# oidc_auth.oidc_logout()
|
||||||
|
app.logger.debug("Logging out current user!")
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@oidc_auth.error_view
|
||||||
|
def error(error=None, error_description=None):
|
||||||
|
app.logger.error(f"Something wwent wrong with OIDC auth – error: {error}, message: {error_description}")
|
||||||
|
return jsonify({'error': error, 'message': error_description})
|
||||||
19
backend/auth/oidc_config.py
Normal file
19
backend/auth/oidc_config.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
|
||||||
|
|
||||||
|
REG_RESPONSE_CLIENT_ID = "lrc-test-bibliothek-kit-edu"
|
||||||
|
REG_RESPONSE_CLIENT_SECRET = "d8531b30-0e6b-4280-b611-1e6c8b4911fa"
|
||||||
|
|
||||||
|
CLIENT_METADATA = ClientMetadata(REG_RESPONSE_CLIENT_ID, REG_RESPONSE_CLIENT_SECRET)
|
||||||
|
|
||||||
|
PROVIDER_URL = "https://oidc.scc.kit.edu/auth/realms/kit"
|
||||||
|
PROVIDER_NAME = 'kit_oidc'
|
||||||
|
PROVIDER_CONFIG = ProviderConfiguration(issuer=PROVIDER_URL,
|
||||||
|
client_metadata=CLIENT_METADATA,
|
||||||
|
auth_request_params={'scope': ['openid', 'email']}
|
||||||
|
# auth_request_params={'scope': ['openid', 'profile']} # avoid to get profile
|
||||||
|
# -> cookie is getting too large
|
||||||
|
# auth_request_params={'scope': ['openid', 'email', 'profile']}
|
||||||
|
)
|
||||||
|
|
||||||
|
OIDC_PROVIDERS = {PROVIDER_NAME: PROVIDER_CONFIG}
|
||||||
23
backend/auth/templates/login.html
Normal file
23
backend/auth/templates/login.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Flask Intro - login page</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Please login</h1>
|
||||||
|
<br>
|
||||||
|
<form action="" method="post">
|
||||||
|
<input type="text" placeholder="E-Mail" name="email" value="{{
|
||||||
|
request.form.username }}">
|
||||||
|
<input type="password" placeholder="Password" name="password" value="{{
|
||||||
|
request.form.password }}">
|
||||||
|
<input class="btn btn-default" type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
{% if error %}
|
||||||
|
<p class="error"><strong>Error:</strong> {{ error }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
backend/auth/templates/login_select.html
Normal file
21
backend/auth/templates/login_select.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Flask Intro - login page</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Please select login method</h1>
|
||||||
|
<br>
|
||||||
|
<ul>
|
||||||
|
{% for provider in providers %}
|
||||||
|
<li><a href="{{url_for(providers[provider].url)}}">{{ provider }} ({{ providers[provider].type }})</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if error %}
|
||||||
|
<p class="error"><strong>Error:</strong> {{ error }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
backend/auth/utils.py
Normal file
36
backend/auth/utils.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import flask_jwt_extended
|
||||||
|
from flask_jwt_extended import jwt_optional, get_jwt_identity
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask_restx import abort
|
||||||
|
|
||||||
|
from backend import jwt_auth
|
||||||
|
from backend.models.user_model import User
|
||||||
|
|
||||||
|
|
||||||
|
def requires_permission_level(permission_level):
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# if flask_jwt_extended.verify_jwt_in_request():
|
||||||
|
current_user_id = get_jwt_identity()
|
||||||
|
user = User.get_by_identifier(current_user_id)
|
||||||
|
if user is not None:
|
||||||
|
if not user.has_permission(permission_level):
|
||||||
|
abort(401, f"You are missing the permission: {permission_level}")
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def require_jwt():
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
return jwt_auth.login_required(jwt_optional(f(*args, **kwargs)))
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
return decorator
|
||||||
33
backend/cert.pem
Normal file
33
backend/cert.pem
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFsDCCA5igAwIBAgIJAPBUPeRmjeGxMA0GCSqGSIb3DQEBCwUAMG0xCzAJBgNV
|
||||||
|
BAYTAkRFMQswCQYDVQQIDAJCVzELMAkGA1UEBwwCS0ExDDAKBgNVBAoMA0tJVDEM
|
||||||
|
MAoGA1UECwwDQklCMSgwJgYDVQQDDB91YmthcHMxNTQudWJrYS51bmkta2FybHNy
|
||||||
|
dWhlLmRlMB4XDTE4MDIwOTEyMTAwOFoXDTE5MDIwOTEyMTAwOFowbTELMAkGA1UE
|
||||||
|
BhMCREUxCzAJBgNVBAgMAkJXMQswCQYDVQQHDAJLQTEMMAoGA1UECgwDS0lUMQww
|
||||||
|
CgYDVQQLDANCSUIxKDAmBgNVBAMMH3Via2FwczE1NC51YmthLnVuaS1rYXJsc3J1
|
||||||
|
aGUuZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDMIX23STpm+2fC
|
||||||
|
zX1SoafxhtDwBbHLc7bhhPqiHV2wp92sj8GVr4TRNPtdxMaAtA6PYJJ+rOZggNi5
|
||||||
|
8ugkQLa9omRRn5Q3V3IQxvUaD5qo0OXI/qHP2xzhBZBzaFwQWfl77kZNibNTYyY0
|
||||||
|
p9Yueg/fMoO7Xeous6DG2cQ57KfNC7vPzTre5f7wMgMgUHf+ziGrYfP8fMIEP2j7
|
||||||
|
u2ULaVgrLhgFrVySJBYDfrBoSU5xIy0U26gcKgAfyX441ybRRRtPThmvW9/MIoH+
|
||||||
|
zRRQAv3SnnnlLH9LAWKEx7OfxjS5Yts8GwJtOJ+TNwO7bIX7TE1+1mZP4hGQHk88
|
||||||
|
JNO/kHM0el2Cbto703F5x0Nk3KCI5oJ9/A+96Dv/8N9F7bQTpbZa89HcEcuwd2iq
|
||||||
|
AsyGVlhEZCyhwsfaNC+IDEBGEfgejvZPBUkuXFLR6F1KwoM3+L4RB2FsI9EYNXjw
|
||||||
|
m/8YCx+2Qn9NoLckf6kJxVYENlLzMWNriyynpmqaQ6XYPPQ1I7OjGqh7jueXV/Bx
|
||||||
|
bvkrUsXW7zbcAuMxVXW1yH8KuXAdMKLTW5gZ3Dj51agErfiRpc06LPPPHaIaCfT8
|
||||||
|
cz4pY3IJQ27jv/fbu2SnpGLuZSADPlWuuLvHKVgtrVS6V3Heay7VQJxDXi5Nzfyd
|
||||||
|
IxGymBfnnjekF4ALJ0f+IZsvDTGojQIDAQABo1MwUTAdBgNVHQ4EFgQU7WjmKeC/
|
||||||
|
yEqGXTK8Qr2irECh5L4wHwYDVR0jBBgwFoAU7WjmKeC/yEqGXTK8Qr2irECh5L4w
|
||||||
|
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAxw/iJGQ0zJaYGobl
|
||||||
|
EmzZX7De/PqiaE/da4Z7Nw2ck5yb0s4z5V5ov41LRWOO/VsaDvP+ucPOrsQzs9uZ
|
||||||
|
ZYpNdb9X1j1ZLLvTepjVFigJO2tXS08oCanSyt5PFjkH/sfyOvrRNr2VPBSB9RDn
|
||||||
|
L8Eru1Cbd2PcZFFO+jm8ioFkRz83uXgTP2t7IZxlc0NUAfT252o+7Y3wf2W4zcXS
|
||||||
|
CiQQTF+isD7F6JVQnmWVLKkBooDE135E3SzesD8bA7VP2OXcld20TMtV6D/HWQKH
|
||||||
|
s/5HlB/D+02+z1kU09xCbpYiUeAk7pRMkR2doAXZ2XjKu5z47H22WD+pzdXn01kX
|
||||||
|
rsxcpRUgv9N+wVScGSK01wcBNNRPXijC/k6hIumlwrYuLN0i3VpJx0sOlnZJ34Gv
|
||||||
|
sVCiW0RDx4mc/HpwMkvNbdKCrra0Q/aX+yjKcAFwelevu0vfj82WF53BAM41aor4
|
||||||
|
yHCDBpFhrfrtOQm3K1jYbUyqTJXbCwNiNs+wstWmDnfkr0qMebXBBGXF81GLrE/4
|
||||||
|
s7LgUuyxu3/dX5Hh/yG7p13LszT3xgk1iEVwEWy6w9Op00pzBDMKGR0Yisq4bzdx
|
||||||
|
ITald/abOFTk+ZwpCOw1HTQO4A6HgwXLAdrVVX45qTP4VsUY8RCcIeEZZtj81SnF
|
||||||
|
xtchwBqM0lLufxWTUOCBIXMVUuU=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
BIN
backend/config.py
Normal file
BIN
backend/config.py
Normal file
Binary file not shown.
102
backend/cron/__init__.py
Normal file
102
backend/cron/__init__.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from backend import app, main_logger
|
||||||
|
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
from backend.cron.cron_state_checker import async_cron_recorder_checker, async_permanent_cron_recorder_checker
|
||||||
|
from backend.websocket.handlers import send_state_update_to_recorders
|
||||||
|
|
||||||
|
cron_log_handler = TimedRotatingFileHandler(app.config.get('CRON_LOG_FILE'), interval=1, when='d', backupCount=3)
|
||||||
|
cron_log_handler.setFormatter(logging.Formatter('[%(asctime)s] - %(funcName)20s() %(message)s'))
|
||||||
|
cron_logger = logging.getLogger("lrc.cron")
|
||||||
|
cron_logger.addHandler(cron_log_handler)
|
||||||
|
logging.getLogger("apscheduler.scheduler").addHandler(cron_log_handler)
|
||||||
|
logging.getLogger("apscheduler.executors.default").addHandler(cron_log_handler)
|
||||||
|
|
||||||
|
scheduler = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_scheduler():
|
||||||
|
cron_logger.debug("creating scheduler!")
|
||||||
|
global scheduler
|
||||||
|
scheduler = BackgroundScheduler(timezone='utc')
|
||||||
|
scheduler.daemonic = False
|
||||||
|
|
||||||
|
return scheduler
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_jobs(sched=None, testing=False):
|
||||||
|
global scheduler
|
||||||
|
if sched is None:
|
||||||
|
sched = scheduler
|
||||||
|
|
||||||
|
if testing:
|
||||||
|
check_recorder_state_job = sched.add_job(async_cron_recorder_checker.check_object_state, 'interval', seconds=40,
|
||||||
|
id="check_recorder_state_job")
|
||||||
|
check_all_recorders_state_job = sched.add_job(async_permanent_cron_recorder_checker, 'interval', minutes=5,
|
||||||
|
id="check_all_recorders_state_job")
|
||||||
|
|
||||||
|
else:
|
||||||
|
check_recorder_state_job = sched.add_job(async_cron_recorder_checker.check_object_state, 'interval', minutes=2,
|
||||||
|
id="check_recorder_state_job")
|
||||||
|
check_all_recorders_state_job = sched.add_job(async_permanent_cron_recorder_checker.check_object_state, 'interval', minutes=30,
|
||||||
|
id="check_all_recorders_state_job")
|
||||||
|
|
||||||
|
"""
|
||||||
|
Job regularly sending the state to "frontend recorders" through websocket
|
||||||
|
"""
|
||||||
|
send_update_state_to_recorder_job = sched.add_job(
|
||||||
|
lambda: send_state_update_to_recorders(async_cron_recorder_checker.get_current_state()), 'interval', minutes=1,
|
||||||
|
id="send_update_state_to_recorder_job")
|
||||||
|
|
||||||
|
return [check_recorder_state_job, send_update_state_to_recorder_job, check_all_recorders_state_job]
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print('You pressed Ctrl+C -> shutting down scheduler!')
|
||||||
|
if scheduler is not None:
|
||||||
|
scheduler.shutdown()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# check_for_ingestibles_and_ingest()
|
||||||
|
# remove_obsolete_media_files_objects()
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setFormatter(
|
||||||
|
logging.Formatter('[%(asctime)s] {%(threadName)s} %(levelname)s in %(module)s, line %(lineno)d: %(message)s'))
|
||||||
|
cron_logger.addHandler(stream_handler)
|
||||||
|
cron_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
get_default_scheduler()
|
||||||
|
add_default_jobs(testing=True)
|
||||||
|
cron_logger.info("Starting internal scheduler")
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
c = 0
|
||||||
|
while c < 10:
|
||||||
|
sleep_time = random.randint(10, 20)
|
||||||
|
cron_logger.info("Sleeping for {}s".format(sleep_time))
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
recorder_id = random.randint(0, 15)
|
||||||
|
cron_logger.info("Using recorder id {}".format(recorder_id))
|
||||||
|
async_cron_recorder_checker.add_object_to_state_check(recorder_id)
|
||||||
|
async_cron_recorder_checker.add_object_to_state_check(recorder_id + 1)
|
||||||
|
pprint(async_cron_recorder_checker.get_current_state())
|
||||||
|
|
||||||
|
while True:
|
||||||
|
user_in = input("Type >exit< to quit.")
|
||||||
|
if user_in == "exit" or user_in == ">exit<":
|
||||||
|
break
|
||||||
|
|
||||||
|
scheduler.shutdown()
|
||||||
175
backend/cron/cron_state_checker.py
Normal file
175
backend/cron/cron_state_checker.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from multiprocessing.context import TimeoutError
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
from pprint import pprint
|
||||||
|
from threading import Lock
|
||||||
|
from typing import Union, Callable, TypeVar, Generic, Set, List
|
||||||
|
|
||||||
|
from backend.models import Recorder
|
||||||
|
from backend.tools.recorder_state_checker import check_capture_agent_state, ping_capture_agent, check_stream_sanity
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.cron.recorder_state")
|
||||||
|
|
||||||
|
recorder_jobs_lock = Lock()
|
||||||
|
recorder_jobs = set()
|
||||||
|
|
||||||
|
NUM_THREADS = 8
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class StateChecker(Generic[T]):
|
||||||
|
"""
|
||||||
|
This class is designed generically to regularly check the state of objects with given function(s).
|
||||||
|
The determined state is stored "locally" in the state checker object and NOT reflected back to the checked objects!
|
||||||
|
It can be retrieved by calling get_current_state.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, state_checker_func: Union[Callable, List[Callable]], type_to_check: T, type_name=None,
|
||||||
|
threads=NUM_THREADS):
|
||||||
|
self.num_threads = threads
|
||||||
|
self.lock = Lock()
|
||||||
|
self.jobs: Set[T] = set()
|
||||||
|
self.checker_func = state_checker_func
|
||||||
|
self.checker_type = type_to_check
|
||||||
|
self.update_state_lock = Lock()
|
||||||
|
self.state_results = {}
|
||||||
|
self.type_name = type_name if type_name is not None else self.checker_type.__name__
|
||||||
|
|
||||||
|
def add_object_to_state_check(self, object_to_check: Union[int, T]):
|
||||||
|
if isinstance(object_to_check, int):
|
||||||
|
if not hasattr(self.checker_type, 'get_by_identifier'):
|
||||||
|
logger.error(
|
||||||
|
'Can\'t add object to state check, as >get_by_identifier< not defined on checker_type ({})!'.format(
|
||||||
|
str(self.checker_type)))
|
||||||
|
return
|
||||||
|
object_to_check = self.checker_type.get_by_identifier(object_to_check)
|
||||||
|
if object_to_check is None:
|
||||||
|
logger.warning(
|
||||||
|
"Could not add object ({}) to state check, as specified >id ({})< could not be found / object is None".format(
|
||||||
|
self.type_name, object_to_check))
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
if hasattr(object_to_check, 'name'):
|
||||||
|
name = object_to_check.name
|
||||||
|
else:
|
||||||
|
name = str(object_to_check)
|
||||||
|
if any([j.id == object_to_check.id for j in self.jobs]):
|
||||||
|
logger.info(
|
||||||
|
"Not adding {} ({}) ({}) to state check (already in job list)".format(object_to_check.id, name,
|
||||||
|
self.type_name))
|
||||||
|
else:
|
||||||
|
logger.debug("Adding {} to object ({}) to state check".format(self.type_name, name))
|
||||||
|
self.jobs.add(object_to_check)
|
||||||
|
|
||||||
|
def remove_recorder_from_state_check(self, object_to_check: Union[int, T]):
|
||||||
|
if isinstance(object_to_check, int):
|
||||||
|
object_to_check = self.checker_type.get_by_identifier(object_to_check)
|
||||||
|
if object_to_check is None:
|
||||||
|
logger.warning(
|
||||||
|
"Could not remove object ({}) from state check, as specified id could not be found / object is None".format(
|
||||||
|
self.type_name))
|
||||||
|
return
|
||||||
|
self.lock.acquire()
|
||||||
|
if hasattr(object_to_check, 'name'):
|
||||||
|
name = object_to_check.name
|
||||||
|
else:
|
||||||
|
name = str(object_to_check)
|
||||||
|
logger.debug("Removing {} from object ({}) to state check".format(self.type_name, name))
|
||||||
|
self.jobs.remove(object_to_check)
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def execute_checker_func(self, func, jobs: List[T], object_states: dict) -> dict:
|
||||||
|
with ThreadPool(self.num_threads) as pool:
|
||||||
|
results = [pool.apply_async(func, (job,)) for job in jobs]
|
||||||
|
try:
|
||||||
|
state_results = [res.get(timeout=12) for res in results]
|
||||||
|
for r in state_results:
|
||||||
|
if r[0]: # ok :)
|
||||||
|
if object_states[r[2]].get('msg', "") == "unknown state!":
|
||||||
|
del object_states[r[2]]['msg']
|
||||||
|
ok = True
|
||||||
|
else:
|
||||||
|
ok = object_states[r[2]].get('state_ok', False),
|
||||||
|
object_states[r[2]] = {
|
||||||
|
'id': object_states[r[2]].get('id', None),
|
||||||
|
'msg': ", ".join([s for s in [object_states[r[2]].get('msg', None), r[1]] if s]),
|
||||||
|
'time_stamp': datetime.datetime.now(datetime.timezone.utc).strftime(
|
||||||
|
"%d.%m.%Y - %H:%M:%S %Z"),
|
||||||
|
'state_ok': ok}
|
||||||
|
else:
|
||||||
|
object_states[r[2]] = {'id': object_states[r[2]].get('id', None),
|
||||||
|
'msg': r[1],
|
||||||
|
'time_stamp': datetime.datetime.now(datetime.timezone.utc).strftime(
|
||||||
|
"%d.%m.%Y - %H:%M:%S %Z"),
|
||||||
|
'state_ok': False}
|
||||||
|
except TimeoutError as e:
|
||||||
|
logger.error("Timeout while performing state check func! {}".format(e))
|
||||||
|
|
||||||
|
return object_states
|
||||||
|
|
||||||
|
def check_object_state(self) -> dict:
|
||||||
|
logger.info("checking object ({}) state...".format(self.type_name))
|
||||||
|
self.lock.acquire()
|
||||||
|
jobs = list(self.jobs)
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
if len(jobs) <= 0:
|
||||||
|
logger.info("No objects ({}) to check... returning".format(self.type_name))
|
||||||
|
return {}
|
||||||
|
logger.info("checking state of {} recorders".format(len(jobs)))
|
||||||
|
|
||||||
|
object_states = {j.name: {'id': j.id, 'state_ok': False, 'msg': 'unknown state!'} for j in jobs}
|
||||||
|
|
||||||
|
if isinstance(self.checker_func, list):
|
||||||
|
for c_f in self.checker_func:
|
||||||
|
self.execute_checker_func(c_f, jobs, object_states)
|
||||||
|
else:
|
||||||
|
self.execute_checker_func(self.checker_func, jobs, object_states)
|
||||||
|
|
||||||
|
self.update_state_dict(object_states)
|
||||||
|
|
||||||
|
return object_states
|
||||||
|
|
||||||
|
def update_state_dict(self, object_states: dict):
|
||||||
|
self.update_state_lock.acquire()
|
||||||
|
for o_s in object_states.keys():
|
||||||
|
if o_s in self.state_results:
|
||||||
|
# update existing state
|
||||||
|
self.state_results[o_s] = {**object_states[o_s],
|
||||||
|
'time_stamp': datetime.datetime.now(datetime.timezone.utc).strftime(
|
||||||
|
"%d.%m.%Y - %H:%M:%S %Z"),
|
||||||
|
'previous': {'state_ok': self.state_results[o_s]['state_ok'],
|
||||||
|
'msg': self.state_results[o_s].get('msg', None),
|
||||||
|
'time_stamp': self.state_results[o_s].get('time_stamp', None)}}
|
||||||
|
else:
|
||||||
|
self.state_results[o_s] = object_states[o_s]
|
||||||
|
|
||||||
|
self.update_state_lock.release()
|
||||||
|
|
||||||
|
def get_current_state(self):
|
||||||
|
with self.update_state_lock:
|
||||||
|
return copy.deepcopy(self.state_results)
|
||||||
|
|
||||||
|
def get_current_state_for_recorder_name(self, recorder_name: str):
|
||||||
|
return self.get_current_state().get(recorder_name, None)
|
||||||
|
|
||||||
|
def get_current_state_for_recorder_id(self, recorder_id: int):
|
||||||
|
states = self.get_current_state()
|
||||||
|
for key in states:
|
||||||
|
state = states[key]
|
||||||
|
if state.get('id', None) == recorder_id: # found!
|
||||||
|
return state
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async_cron_recorder_checker = StateChecker([check_capture_agent_state, ping_capture_agent], Recorder)
|
||||||
|
async_permanent_cron_recorder_checker = StateChecker(
|
||||||
|
[check_capture_agent_state, ping_capture_agent, check_stream_sanity], Recorder)
|
||||||
|
|
||||||
|
#for r in Recorder.get_all():
|
||||||
|
# async_permanent_cron_recorder_checker.add_object_to_state_check(r.id)
|
||||||
|
|
||||||
52
backend/key.pem
Normal file
52
backend/key.pem
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDMIX23STpm+2fC
|
||||||
|
zX1SoafxhtDwBbHLc7bhhPqiHV2wp92sj8GVr4TRNPtdxMaAtA6PYJJ+rOZggNi5
|
||||||
|
8ugkQLa9omRRn5Q3V3IQxvUaD5qo0OXI/qHP2xzhBZBzaFwQWfl77kZNibNTYyY0
|
||||||
|
p9Yueg/fMoO7Xeous6DG2cQ57KfNC7vPzTre5f7wMgMgUHf+ziGrYfP8fMIEP2j7
|
||||||
|
u2ULaVgrLhgFrVySJBYDfrBoSU5xIy0U26gcKgAfyX441ybRRRtPThmvW9/MIoH+
|
||||||
|
zRRQAv3SnnnlLH9LAWKEx7OfxjS5Yts8GwJtOJ+TNwO7bIX7TE1+1mZP4hGQHk88
|
||||||
|
JNO/kHM0el2Cbto703F5x0Nk3KCI5oJ9/A+96Dv/8N9F7bQTpbZa89HcEcuwd2iq
|
||||||
|
AsyGVlhEZCyhwsfaNC+IDEBGEfgejvZPBUkuXFLR6F1KwoM3+L4RB2FsI9EYNXjw
|
||||||
|
m/8YCx+2Qn9NoLckf6kJxVYENlLzMWNriyynpmqaQ6XYPPQ1I7OjGqh7jueXV/Bx
|
||||||
|
bvkrUsXW7zbcAuMxVXW1yH8KuXAdMKLTW5gZ3Dj51agErfiRpc06LPPPHaIaCfT8
|
||||||
|
cz4pY3IJQ27jv/fbu2SnpGLuZSADPlWuuLvHKVgtrVS6V3Heay7VQJxDXi5Nzfyd
|
||||||
|
IxGymBfnnjekF4ALJ0f+IZsvDTGojQIDAQABAoICAFtD2gY5WkAyxOhmoVJxbjnh
|
||||||
|
MccudJhm6uwXXUtf38ScuNJvD3kSGUrD6mK5GJrwZdYaskSqnvGkicFRZhLXUBym
|
||||||
|
3z1TIJxBn4D6wxjcwyQZCbN8jPH2oAnHSBchIJA6+f07wfjmyONOYAWIyIzNDA2Z
|
||||||
|
cyYxTgOFUiu1rzLKqRdW2KiGtHx1zi6r3ZP0BkJI/Oq4B7LqQIBPrWtnFD0u4zmj
|
||||||
|
CSB4qvu34JO9b7egls6kkIJT1uyIprePibx2DSfhQt2JKJirdfq5ru8x15QSUlN5
|
||||||
|
gTxCUcNCIj8FXjzgDDCajzRnSwV0HHxgS23fjmOVcMx1pRawF3Qc6QYV3Qo31hli
|
||||||
|
E3fZAFwNkob/adeSPLlly8fFOcLuapMGnLIXYTY4f1bcfu7+x5gIUy17RFlbC0q3
|
||||||
|
wDxQwopc5AUmJSCvCt2PqSjGfIOcT8mbq9Zc9x7i3FpyarqmUEuSOUi5ufioJEve
|
||||||
|
lrjyRuBH35vmM565b1UVpsPMPIZoMhGJ9VVvhosfu5QSANv0DyToWAffBbUpXnt4
|
||||||
|
MnDpJR3S+JpdrYScz/jvIBxhgzrUeYFJ+2YXqFSRLKECwoG3JvBr6lmPP9o+tyyv
|
||||||
|
nQzsrBZ7IueWfu+w+9d8uPqVrXlJcTXf+6u/WV91DOfJGwfYhimFglMtmyTpq9cb
|
||||||
|
6oXtq2NiAJjGvPxmJ1GNAoIBAQD66pKZW3l3NWQF1ku+hJaNGU8ZbQ1oCTFXZcM/
|
||||||
|
bel6CZgVY366deLjUPnJyfe7cRG+c44hN+mvOg+s4GwOI8f/felOpZp66Nqum5+e
|
||||||
|
RRqP4SiF8Xo/y4NnB//HipeHEEEVQXTOQNewE4uYflFzcYhlm/CXSEfKTdJVMgL8
|
||||||
|
n+C8tAUeVvm3dyJUjGSRB0RWuaADffRh13XlZokIrE31fVceTCNnLDiwzoiInItl
|
||||||
|
AUME88apYi/sjS4o1kS2hO2v/0ZsTl6Ibc2peMTs2xHUDzw9Gop8aV0nevNb2Ale
|
||||||
|
bfvi54fzJiVTIqI69FnfSiceosPmCRyq5SoFdKmHuBDQGEJjAoIBAQDQREGZOec4
|
||||||
|
zS63qOKHXCzCeHfAQoecSrvJjmcq50JdmfKQqDCcV8MB7ljXToKflJeuNzO4xwiK
|
||||||
|
xjh+4JUobqlYwyNBQZMxG8Vpmsn8DHSjlx16vkeFTjZP8Nl1ZnVd2HexpyXvHKnM
|
||||||
|
x8aWmSbE9ZzDM231/q8zuw1jvN69pJuANz8or1wAs+x6NpccS48MBZojziStbQAZ
|
||||||
|
rH0QsWdOkBwug8YolYi8u9ir9xt+bqR9fU0uhy1V2736Gc2t100iho4vVBA7dAuc
|
||||||
|
7BSKO+j5SIy1DwI+S8P/hLGpp7bThimcsYkQGo4Wzi3zBGmaV4ZCl4wgqS3TW2EQ
|
||||||
|
LVlFvoAkp+RPAoIBAQCUSUroHPpU7BW3qWTMLDl5G8r2YM96e2xQlVBlTQSdXcwm
|
||||||
|
X82GTqMO6k0k5xpkCTeOUWZe1VdiejLHXT4ewSCyKmxWUWJRXwnWBRy5AWfoPg2f
|
||||||
|
0w0HZLO8kSqld4+Df6Sm9i8csY+GfJaUQZrLWf5c5mKyVUIwGfvC47KGL8o2W0Fn
|
||||||
|
I8milmKQiwPn/d5yTnN1fNuPczE2qHk1yfasrS1uN1r431aFjxl7euCaibtc0uDG
|
||||||
|
O8PMzcbI1ZB4OWjrCTdKTKprgFD55eijffg0Veiox+WuDurBqVTnI26uAtvIxkI+
|
||||||
|
/X0ze5VauAvg/UbPQSv658msaZCC9uY10h8FjEC3AoIBAG+wSyWrIc8aajVgQquo
|
||||||
|
yPA5vq7CfwtYIMEEt1sQzkx4JNdi+z686f19HvPITNDb1UQ+omQziOczSlTwh805
|
||||||
|
G2RYse1rB07Mv9/UfQHIhDy+67ZJmP1qZkIUvenx2ntLfVUueG91BbKmaF+XHm19
|
||||||
|
8mXUjOHhhX/Ojm2wehtlzWbDOgHNmR9fXjBkWkF4W+xsjK8q/AxtaiJamG99VBOT
|
||||||
|
wSlIzdox5zSf4KDIUlxJZblOmzeakrt6rrUTZXQXBGzBkDrdcB8SKrArAxDm9BfL
|
||||||
|
ynnG5MMXyrKbLNP491kUl/hKVWDnfM/KHmY3NZLp4TyRHTrev46bcMBGMZvvf7Uo
|
||||||
|
vI0CggEAdL9KcgPrK6/3DtaY2XcQAUjrMXjnthgJWUtZkdtZ7xQrLIaRMW0FSgQd
|
||||||
|
5U3eoHrXTmnFCRSTE87BA1jUPlZ/iaQw9Spfuz86IYoyu+lLOGfhaD/qVZ3lWUj4
|
||||||
|
/S95K/9561PDflWqO2BQ77UhfrXOyLPFsvUpJspZ7RuIrX0OPWplLlTRAM2zEJOd
|
||||||
|
Q66dS3Wle7Xkz0ypo4HWzYc4uqHHtbGEVmCLlqxLJFBjtlHFAbLY//PdiU/nMGWB
|
||||||
|
MYka0asxUegydpH/t6FZD426lhDp8P2KB7xk4XV47s2KfIsemeteE4vVFT+wRnel
|
||||||
|
MyJDELvA01ZFZW9xXMwfxTk7k7fUCA==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
98
backend/manage.py
Normal file
98
backend/manage.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os, sys
|
||||||
|
|
||||||
|
from backend.models import Permission, Group
|
||||||
|
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir))
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import coverage
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from flask_script import Manager
|
||||||
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
|
|
||||||
|
from backend import app, db
|
||||||
|
|
||||||
|
COV = coverage.coverage(
|
||||||
|
branch=True,
|
||||||
|
include='app/*',
|
||||||
|
omit=[
|
||||||
|
'app/tests/*',
|
||||||
|
'app/server/config.py',
|
||||||
|
'app/server/*/__init__.py'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
COV.start()
|
||||||
|
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
manager = Manager(app)
|
||||||
|
|
||||||
|
# migrations
|
||||||
|
manager.add_command('db', MigrateCommand)
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def test():
|
||||||
|
"""Runs the unit tests without test coverage."""
|
||||||
|
tests = unittest.TestLoader().discover('tests', pattern='test*.py')
|
||||||
|
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
||||||
|
if result.wasSuccessful():
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def cov():
|
||||||
|
"""Runs the unit tests with coverage."""
|
||||||
|
tests = unittest.TestLoader().discover('app/tests')
|
||||||
|
result = unittest.TextTestRunner(verbosity=2).run(tests)
|
||||||
|
if result.wasSuccessful():
|
||||||
|
COV.stop()
|
||||||
|
COV.save()
|
||||||
|
print('Coverage Summary:')
|
||||||
|
COV.report()
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
covdir = os.path.join(basedir, 'tmp/coverage')
|
||||||
|
COV.html_report(directory=covdir)
|
||||||
|
print('HTML version: file://%s/index.html' % covdir)
|
||||||
|
COV.erase()
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def insert_initial_groups():
|
||||||
|
print("DB: inserting default groups:")
|
||||||
|
for g in app.config.get("GROUPS", []):
|
||||||
|
print(g['name'])
|
||||||
|
g_permissions = g.pop('permissions', [])
|
||||||
|
g['permissions'] = Permission.get_by_names(g_permissions)
|
||||||
|
db.session.add(Group(**g))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def recreate_db():
|
||||||
|
"""Drops the db tables."""
|
||||||
|
db.drop_all()
|
||||||
|
"""Creates the db tables."""
|
||||||
|
db.create_all()
|
||||||
|
insert_initial_groups()
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def create_db():
|
||||||
|
"""Creates the db tables."""
|
||||||
|
db.create_all()
|
||||||
|
insert_initial_groups()
|
||||||
|
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def drop_db():
|
||||||
|
"""Drops the db tables."""
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
manager.run()
|
||||||
1
backend/migrations/README
Normal file
1
backend/migrations/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
45
backend/migrations/alembic.ini
Normal file
45
backend/migrations/alembic.ini
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
95
backend/migrations/env.py
Normal file
95
backend/migrations/env.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
from flask import current_app
|
||||||
|
config.set_main_option('sqlalchemy.url',
|
||||||
|
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||||
|
target_metadata = current_app.extensions['migrate'].db.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 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 = config.get_main_option("sqlalchemy.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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
process_revision_directives=process_revision_directives,
|
||||||
|
**current_app.extensions['migrate'].configure_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
backend/migrations/script.py.mako
Normal file
24
backend/migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${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 upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
136
backend/migrations/versions/b154e49921e0_.py
Normal file
136
backend/migrations/versions/b154e49921e0_.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: b154e49921e0
|
||||||
|
Revises:
|
||||||
|
Create Date: 2019-04-11 14:44:25.836132
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b154e49921e0'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('blacklist_tokens',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('token', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('blacklisted_on', sa.DateTime(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('token')
|
||||||
|
)
|
||||||
|
op.create_table('example_data_item',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('mac', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('uuid', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('some_string_value', sa.String(), nullable=True),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('description', sa.String(length=4096), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_example_data_item_mac'), 'example_data_item', ['mac'], unique=True)
|
||||||
|
op.create_index(op.f('ix_example_data_item_name'), 'example_data_item', ['name'], unique=False)
|
||||||
|
op.create_index(op.f('ix_example_data_item_some_string_value'), 'example_data_item', ['some_string_value'], unique=False)
|
||||||
|
op.create_index(op.f('ix_example_data_item_uuid'), 'example_data_item', ['uuid'], unique=True)
|
||||||
|
op.create_table('group',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.Unicode(length=63), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('permission',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.Unicode(length=63), nullable=False),
|
||||||
|
sa.Column('description', sa.Unicode(length=255), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('group_permission',
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('permission_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['group.id'], onupdate='CASCADE', ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], onupdate='CASCADE', ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('group_id', 'permission_id')
|
||||||
|
)
|
||||||
|
op.create_table('user',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('social_id', sa.Unicode(length=63), nullable=True),
|
||||||
|
sa.Column('nickname', sa.Unicode(length=63), nullable=True),
|
||||||
|
sa.Column('first_name', sa.Unicode(length=63), nullable=True),
|
||||||
|
sa.Column('last_name', sa.Unicode(length=63), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=120), nullable=False),
|
||||||
|
sa.Column('lang', sa.Unicode(length=32), nullable=True),
|
||||||
|
sa.Column('timezone', sa.Unicode(length=63), nullable=True),
|
||||||
|
sa.Column('example_data_item_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('about_me', sa.Unicode(length=255), nullable=True),
|
||||||
|
sa.Column('role', sa.Unicode(length=63), nullable=True),
|
||||||
|
sa.Column('password', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('registered_on', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('external_user', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('last_seen', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('jwt_exp_delta_seconds', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['example_data_item_id'], ['example_data_item.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('social_id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||||
|
op.create_index(op.f('ix_user_first_name'), 'user', ['first_name'], unique=False)
|
||||||
|
op.create_index(op.f('ix_user_last_name'), 'user', ['last_name'], unique=False)
|
||||||
|
op.create_index(op.f('ix_user_nickname'), 'user', ['nickname'], unique=True)
|
||||||
|
op.create_table('acquaintances',
|
||||||
|
sa.Column('me_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('acquaintance_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['acquaintance_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['me_id'], ['user.id'], )
|
||||||
|
)
|
||||||
|
op.create_table('followers',
|
||||||
|
sa.Column('follower_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('followed_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['follower_id'], ['user.id'], )
|
||||||
|
)
|
||||||
|
op.create_table('post',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('body', sa.String(length=140), nullable=True),
|
||||||
|
sa.Column('timestamp', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('user_group',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['group.id'], onupdate='CASCADE', ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], onupdate='CASCADE', ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'group_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('user_group')
|
||||||
|
op.drop_table('post')
|
||||||
|
op.drop_table('followers')
|
||||||
|
op.drop_table('acquaintances')
|
||||||
|
op.drop_index(op.f('ix_user_nickname'), table_name='user')
|
||||||
|
op.drop_index(op.f('ix_user_last_name'), table_name='user')
|
||||||
|
op.drop_index(op.f('ix_user_first_name'), table_name='user')
|
||||||
|
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||||
|
op.drop_table('user')
|
||||||
|
op.drop_table('group_permission')
|
||||||
|
op.drop_table('permission')
|
||||||
|
op.drop_table('group')
|
||||||
|
op.drop_index(op.f('ix_example_data_item_uuid'), table_name='example_data_item')
|
||||||
|
op.drop_index(op.f('ix_example_data_item_some_string_value'), table_name='example_data_item')
|
||||||
|
op.drop_index(op.f('ix_example_data_item_name'), table_name='example_data_item')
|
||||||
|
op.drop_index(op.f('ix_example_data_item_mac'), table_name='example_data_item')
|
||||||
|
op.drop_table('example_data_item')
|
||||||
|
op.drop_table('blacklist_tokens')
|
||||||
|
# ### end Alembic commands ###
|
||||||
10
backend/models/__init__.py
Normal file
10
backend/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Import all models...
|
||||||
|
"""
|
||||||
|
from backend.models.access_control_model import *
|
||||||
|
from backend.models.example_model import *
|
||||||
|
from backend.models.post_model import *
|
||||||
|
from backend.models.recorder_model import *
|
||||||
|
from backend.models.room_model import *
|
||||||
|
from backend.models.user_model import *
|
||||||
|
from backend.models.virtual_command_model import *
|
||||||
72
backend/models/access_control_model.py
Normal file
72
backend/models/access_control_model.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Models for lecture recorder
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from sqlalchemy import MetaData, CheckConstraint
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from backend import db, app, login_manager
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class AccessControlEntry(db.Model):
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
name = db.Column(db.Unicode(127), unique=False, nullable=False)
|
||||||
|
url = db.Column(db.Unicode(2047), unique=False, nullable=True, default="")
|
||||||
|
|
||||||
|
required_permission_id = db.Column(db.Integer, db.ForeignKey('permission.id'))
|
||||||
|
required_permission = db.relationship('Permission', back_populates='access_control_entry')
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint('length(name) > 2',
|
||||||
|
name='name_min_length'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(AccessControlEntry, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
"""
|
||||||
|
Find ace by name
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return AccessControlEntry.query.filter(AccessControlEntry.name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Return all ace
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return AccessControlEntry.query.all()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(id=self.id, name=self.name)
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
|
sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def pre_fill_table():
|
||||||
|
a_es = {"url": "", }
|
||||||
|
access_entries = [AccessControlEntry(name=room['name'], number=room['room_number'],
|
||||||
|
building_name=room['building_name'], building_number=room['building_number']) for room in
|
||||||
|
a_es]
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.bulk_save_objects(access_entries)
|
||||||
|
db.session.commit()
|
||||||
|
except IntegrityError as e:
|
||||||
|
db.session.rollback()
|
||||||
BIN
backend/models/initial_recorders.json
Normal file
BIN
backend/models/initial_recorders.json
Normal file
Binary file not shown.
11
backend/models/model_base.py
Normal file
11
backend/models/model_base.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
class ModelBase:
|
||||||
|
def get(self, attribute_name, default_value=None):
|
||||||
|
if hasattr(self, attribute_name):
|
||||||
|
return getattr(self, attribute_name)
|
||||||
|
elif False: # a check for properties?
|
||||||
|
pass
|
||||||
|
elif default_value is not None:
|
||||||
|
return default_value
|
||||||
|
else:
|
||||||
|
raise KeyError("{} not found".format(attribute_name))
|
||||||
312
backend/models/recorder_model.py
Normal file
312
backend/models/recorder_model.py
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Models for lecture recorder
|
||||||
|
"""
|
||||||
|
import importlib
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import pkgutil
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from sqlalchemy import MetaData, ForeignKeyConstraint
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from sqlalchemy.orm import validates
|
||||||
|
|
||||||
|
from backend import db, app, login_manager, LrcException
|
||||||
|
from sqlalchemy import or_
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from backend.models.model_base import ModelBase
|
||||||
|
from backend.models.virtual_command_model import virtual_command_recorder_command_table, \
|
||||||
|
virtual_command_recorder_table, \
|
||||||
|
virtual_command_recorder_model_table
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# recorders and permissions.
|
||||||
|
recorder_permission_table = db.Table('recorder_permission',
|
||||||
|
db.Column('recorder_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('permission_id', db.Integer,
|
||||||
|
db.ForeignKey('permission.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
extend_existing=True)
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# recorders and recorderCommands and permissions.
|
||||||
|
recorder_rec_commands_permission_table = db.Table('recorder_rec_command_permission',
|
||||||
|
db.Column('recorder_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('recorder_command_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder_command.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('permission_id', db.Integer,
|
||||||
|
db.ForeignKey('permission.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
extend_existing=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RecorderModel(db.Model, ModelBase):
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
last_time_modified = db.Column(db.DateTime, nullable=True, default=None)
|
||||||
|
record_adapter_id = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
model_name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
notes = db.Column(db.Unicode(255), unique=False, nullable=True, default=None)
|
||||||
|
recorder_commands = db.relationship('RecorderCommand', back_populates='recorder_model')
|
||||||
|
recorders = db.relationship('Recorder', back_populates='recorder_model')
|
||||||
|
checksum = db.Column(db.String(63), unique=True,
|
||||||
|
nullable=False) # checksum of the recorder commands! (see: model_updater.py)
|
||||||
|
last_checksum_change = db.Column(db.DateTime, nullable=True, default=None)
|
||||||
|
virtual_commands = db.relationship('VirtualCommand', secondary=virtual_command_recorder_model_table,
|
||||||
|
back_populates='recorder_model_commands')
|
||||||
|
_requires_user = db.Column(db.Integer, default=False, name='requires_user')
|
||||||
|
_requires_password = db.Column(db.Integer, default=True, name='requires_password')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
return RecorderModel.query.all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(id):
|
||||||
|
return RecorderModel.query.filter(RecorderModel.id == id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
return RecorderModel.query.filter(RecorderModel.model_name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_adapter_id(name):
|
||||||
|
return RecorderModel.query.filter(RecorderModel.record_adapter_id == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_where_adapter_id_contains(adapter_id):
|
||||||
|
return RecorderModel.query.filter(RecorderModel.record_adapter_id.contains(adapter_id)).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_checksum(md5_sum):
|
||||||
|
return RecorderModel.query.filter(RecorderModel.checksum == md5_sum).first()
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def requires_username(self):
|
||||||
|
return self._requires_user > 0
|
||||||
|
|
||||||
|
@requires_username.setter
|
||||||
|
def requires_username(self, val: bool):
|
||||||
|
self._requires_user = 1 if val else 0
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def requires_password(self):
|
||||||
|
return self._requires_password > 0
|
||||||
|
|
||||||
|
@requires_password.setter
|
||||||
|
def requires_password(self, val: bool):
|
||||||
|
self._requires_password = 1 if val else 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.model_name + " (record adapter: {})".format(self.record_adapter_id)
|
||||||
|
|
||||||
|
|
||||||
|
class Recorder(db.Model, ModelBase):
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
last_time_modified = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
model_name = db.Column(db.Unicode(63), unique=False, nullable=False)
|
||||||
|
serial_number = db.Column(db.Unicode(63), unique=True, nullable=True)
|
||||||
|
locked = db.Column(db.Boolean, default=False) # no modifications allowed
|
||||||
|
lock_message = db.Column(db.String, nullable=True, default=None)
|
||||||
|
offline = db.Column(db.Boolean, default=False) # maintenance, etc.
|
||||||
|
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
|
||||||
|
_mac = db.Column(db.String(17), unique=True, nullable=True)
|
||||||
|
_ip = db.Column(db.String(15), unique=True, nullable=True, default=None)
|
||||||
|
_ip6 = db.Column(db.String(46), unique=True, nullable=True, default=None)
|
||||||
|
network_name = db.Column(db.String(127), unique=True, nullable=True, default=None)
|
||||||
|
telnet_port = db.Column(db.Integer, unique=False, nullable=False, default=23)
|
||||||
|
ssh_port = db.Column(db.Integer, unique=False, nullable=False, default=22)
|
||||||
|
username = db.Column(db.String, nullable=True, default=None)
|
||||||
|
password = db.Column(db.String, nullable=True, default=None)
|
||||||
|
_configured_options_json_string = db.Column(db.UnicodeText, default='')
|
||||||
|
_additional_notes_json_string = db.Column(db.UnicodeText, default='')
|
||||||
|
additional_camera_connected = db.Column(db.Boolean, default=False)
|
||||||
|
firmware_version = db.Column(db.String, nullable=True, default=None)
|
||||||
|
archive_stream1 = db.Column(db.String, nullable=True, default=None)
|
||||||
|
archive_stream2 = db.Column(db.String, nullable=True, default=None)
|
||||||
|
confidence_stream = db.Column(db.String, nullable=True, default=None)
|
||||||
|
room_id = db.Column(db.Integer, db.ForeignKey('room.id'))
|
||||||
|
room = db.relationship('Room', uselist=False, back_populates='recorder') # one-to-one relation (uselist=False)
|
||||||
|
recorder_model_id = db.Column(db.Integer, db.ForeignKey('recorder_model.id'))
|
||||||
|
recorder_model = db.relationship('RecorderModel', back_populates='recorders')
|
||||||
|
virtual_commands = db.relationship('VirtualCommand', secondary=virtual_command_recorder_table,
|
||||||
|
back_populates='recorders')
|
||||||
|
|
||||||
|
required_read_permissions = db.relationship('Permission', secondary=recorder_permission_table)
|
||||||
|
required_write_permissions = db.relationship('Permission', secondary=recorder_permission_table)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Recorder, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
return Recorder.query.filter(Recorder.name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_identifier(identifier):
|
||||||
|
return Recorder.query.filter(Recorder.id == identifier).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_mac(mac: str):
|
||||||
|
if mac is None or mac == '':
|
||||||
|
return None
|
||||||
|
mac = mac.replace('-', ':').lower()
|
||||||
|
return Recorder.query.filter(Recorder._mac == mac).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
return Recorder.query.all()
|
||||||
|
|
||||||
|
@validates('name')
|
||||||
|
def validate_name(self, key, value):
|
||||||
|
assert len(value) > 2
|
||||||
|
return value
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def configured_options(self) -> list:
|
||||||
|
return json.loads(self._configured_options_json_string)
|
||||||
|
|
||||||
|
@configured_options.setter
|
||||||
|
def configured_options(self, value: list):
|
||||||
|
self._configured_options_json_string = json.dumps(value)
|
||||||
|
|
||||||
|
def add_configured_option(self, value: str):
|
||||||
|
self._configured_options_json_string = json.dumps(self.configured_options.append(value))
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def additional_notes(self) -> list:
|
||||||
|
return json.loads(self._additional_notes_json_string)
|
||||||
|
|
||||||
|
@additional_notes.setter
|
||||||
|
def additional_notes(self, value: list):
|
||||||
|
self._additional_notes_json_string = json.dumps(value)
|
||||||
|
|
||||||
|
def add_additional_notes(self, value: str):
|
||||||
|
self._additional_notes_json_string = json.dumps(self._additional_notes_json_string.append(value))
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def mac(self) -> str:
|
||||||
|
return self._mac.upper()
|
||||||
|
|
||||||
|
@mac.setter
|
||||||
|
def mac(self, value: str):
|
||||||
|
if value is None or value == '':
|
||||||
|
return
|
||||||
|
if re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", value.lower()):
|
||||||
|
self._mac = value.replace('-', ':').lower()
|
||||||
|
else:
|
||||||
|
raise LrcException("'{}' is not a valid MAC Address!".format(value))
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def ip(self) -> str:
|
||||||
|
return self._ip
|
||||||
|
|
||||||
|
@ip.setter
|
||||||
|
def ip(self, value: str):
|
||||||
|
try:
|
||||||
|
ipaddress.IPv4Interface(value)
|
||||||
|
self._ip = value
|
||||||
|
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
|
||||||
|
raise LrcException("'{}' is not a valid IPv4 Address!".format(value))
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def ip6(self) -> str:
|
||||||
|
return self._ip6
|
||||||
|
|
||||||
|
@ip6.setter
|
||||||
|
def ip6(self, value: str):
|
||||||
|
try:
|
||||||
|
ipaddress.IPv6Interface(value)
|
||||||
|
self._ip6 = value
|
||||||
|
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
|
||||||
|
raise LrcException("'{}' is not a valid IPv6 Address!".format(value))
|
||||||
|
|
||||||
|
# handle bad ip
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(id=self.id, name=self.name)
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
|
sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
class RecorderCommandPermission(db.Model, ModelBase):
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
recorder = db.relationship('Recorder')
|
||||||
|
recorder_id = db.Column(db.Integer, db.ForeignKey('recorder.id'))
|
||||||
|
recorder_command = db.relationship('RecorderCommand')
|
||||||
|
recorder_command_id = db.Column(db.Integer, db.ForeignKey('recorder_command.id'))
|
||||||
|
permission = db.relationship('Permission')
|
||||||
|
permission_id = db.Column(db.Integer, db.ForeignKey('permission.id'))
|
||||||
|
|
||||||
|
|
||||||
|
class RecorderCommand(db.Model, ModelBase):
|
||||||
|
__table_args__ = {'extend_existing': True}
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
last_time_modified = db.Column(db.DateTime, nullable=True, default=datetime.utcnow())
|
||||||
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
alternative_name = db.Column(db.Unicode(63), unique=True, nullable=True, default=None)
|
||||||
|
disabled = db.Column(db.Boolean, default=False)
|
||||||
|
description = db.Column(db.Unicode(511), nullable=True, default=None)
|
||||||
|
parameters_string = db.Column(db.String(2047), nullable=True)
|
||||||
|
recorder_model = db.relationship('RecorderModel', back_populates='recorder_commands')
|
||||||
|
recorder_model_id = db.Column(db.Integer, db.ForeignKey('recorder_model.id'))
|
||||||
|
virtual_commands = db.relationship('VirtualCommand', secondary=virtual_command_recorder_command_table,
|
||||||
|
back_populates='recorder_commands')
|
||||||
|
|
||||||
|
required_show_permissions = db.relationship('RecorderCommandPermission')
|
||||||
|
required_execute_permissions = db.relationship('RecorderCommandPermission')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
return RecorderCommand.query.all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_identifier(identifier):
|
||||||
|
return RecorderCommand.query.filter(RecorderCommand.id == identifier).first()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parameters(self) -> Union[dict, None]:
|
||||||
|
if self.parameters_string is None:
|
||||||
|
return None
|
||||||
|
return json.loads(self.parameters_string)
|
||||||
|
|
||||||
|
@parameters.setter
|
||||||
|
def parameters(self, parameters_dict: dict):
|
||||||
|
self.parameters_string = json.dumps(parameters_dict)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
Recorder(name="RecTest")
|
||||||
97
backend/models/room_model.py
Normal file
97
backend/models/room_model.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Models for lecture recorder
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import MetaData, CheckConstraint, UniqueConstraint
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from backend import db, app, login_manager
|
||||||
|
from backend.tools.campus_rooms import get_campus_rooms
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc." + __name__)
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class Room(db.Model):
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
name = db.Column(db.Unicode(127), unique=False, nullable=False)
|
||||||
|
alternate_name = db.Column(db.Unicode(127), unique=False, nullable=True, default=None)
|
||||||
|
external_id = db.Column(db.String, nullable=True, unique=True)
|
||||||
|
coordinates = db.Column(db.String, nullable=True)
|
||||||
|
comment = db.Column(db.Unicode(2047), unique=False, nullable=True, default="")
|
||||||
|
number = db.Column(db.Unicode(63), unique=False, nullable=True, default=None)
|
||||||
|
building_name = db.Column(db.Unicode(63), unique=False, nullable=True)
|
||||||
|
building_number = db.Column(db.Unicode(63), unique=False, nullable=True, default=None)
|
||||||
|
|
||||||
|
recorder = db.relationship('Recorder', uselist=False, back_populates='room') # one-to-one relation (uselist=False)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint('length(name) > 2',
|
||||||
|
name='name_min_length'),
|
||||||
|
UniqueConstraint('name', 'number', 'building_number', 'external_id'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Room, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
"""
|
||||||
|
Find group by name
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Room.query.filter(Room.name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_building_number(building_number):
|
||||||
|
"""
|
||||||
|
Find group by name
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Room.query.filter(Room.building_number == building_number)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Return all rooms
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Room.query.all()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(id=self.id, name=self.name)
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
|
sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def pre_fill_table():
|
||||||
|
# Room.query.delete() # drop all rooms
|
||||||
|
rooms = get_campus_rooms()
|
||||||
|
logger.debug("got {} rooms".format(len(rooms)))
|
||||||
|
db_rooms = [Room(name=room['name'], number=room['room_number'], alternate_name=room.get('alternate_name', None),
|
||||||
|
building_name=room['building_name'], building_number=room['building_number'],
|
||||||
|
coordinates=room.get('coordinates', None), external_id=room.get('external_id', None)
|
||||||
|
|
||||||
|
) for room in
|
||||||
|
rooms]
|
||||||
|
try:
|
||||||
|
db.session.bulk_save_objects(db_rooms)
|
||||||
|
db.session.commit()
|
||||||
|
logger.debug("rooms commited to DB!")
|
||||||
|
except IntegrityError as e:
|
||||||
|
logger.error("Could not add rooms! ({})".format(e))
|
||||||
|
db.session.rollback()
|
||||||
594
backend/models/user_model.py
Normal file
594
backend/models/user_model.py
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Example user model and related models
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
from sqlalchemy.orm import relation, validates
|
||||||
|
from sqlalchemy import MetaData, any_
|
||||||
|
|
||||||
|
from backend import db, app, login_manager
|
||||||
|
from backend.config import Config
|
||||||
|
from backend.models.post_model import Post
|
||||||
|
from backend.models.example_model import ExampleDataItem
|
||||||
|
import re
|
||||||
|
import jwt
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from sqlalchemy import or_, event
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from passlib.hash import sha256_crypt
|
||||||
|
from hashlib import md5
|
||||||
|
|
||||||
|
|
||||||
|
metadata = MetaData()
|
||||||
|
|
||||||
|
followers = db.Table('followers',
|
||||||
|
db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
|
||||||
|
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
acquaintances = db.Table('acquaintances',
|
||||||
|
db.Column('me_id', db.Integer, db.ForeignKey('user.id')),
|
||||||
|
db.Column('acquaintance_id', db.Integer, db.ForeignKey('user.id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
user_favorite_recorders_table = db.Table('user_favorite_recorders',
|
||||||
|
db.Column('user_id', db.Integer,
|
||||||
|
db.ForeignKey('user.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('recorder_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# groups and members - this is, the memberships.
|
||||||
|
user_group_table = db.Table('user_group',
|
||||||
|
db.Column('user_id', db.Integer,
|
||||||
|
db.ForeignKey('user.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('group_id', db.Integer,
|
||||||
|
db.ForeignKey('group.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# groups and permissions.
|
||||||
|
group_permission_table = db.Table('group_permission',
|
||||||
|
db.Column('group_id', db.Integer,
|
||||||
|
db.ForeignKey('group.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('permission_id', db.Integer,
|
||||||
|
db.ForeignKey('permission.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# users and permissions.
|
||||||
|
user_permission_table = db.Table('user_permission',
|
||||||
|
db.Column('user_id', db.Integer,
|
||||||
|
db.ForeignKey('user.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('permission_id', db.Integer,
|
||||||
|
db.ForeignKey('permission.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
"""
|
||||||
|
Example user model representation.
|
||||||
|
"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
social_id = db.Column(db.Unicode(63), nullable=True, unique=True)
|
||||||
|
nickname = db.Column(db.Unicode(63), index=True, unique=True)
|
||||||
|
first_name = db.Column(db.Unicode(63), index=True, nullable=True)
|
||||||
|
last_name = db.Column(db.Unicode(63), index=True, nullable=True)
|
||||||
|
email = db.Column(db.String(120), nullable=False, index=True, unique=True)
|
||||||
|
lang = db.Column(db.Unicode(32), index=False, unique=False)
|
||||||
|
timezone = db.Column(db.Unicode(63), index=False, unique=False)
|
||||||
|
posts = db.relationship('Post', backref='author', lazy='dynamic')
|
||||||
|
example_data_item = db.relationship('ExampleDataItem', backref='owner')
|
||||||
|
example_data_item_id = db.Column(db.ForeignKey(ExampleDataItem.id))
|
||||||
|
about_me = db.Column(db.Unicode(255))
|
||||||
|
role = db.Column(db.Unicode(63))
|
||||||
|
groups = db.relationship('Group', secondary=user_group_table, back_populates='users')
|
||||||
|
permissions = db.relationship('Permission', secondary=user_permission_table, back_populates='users')
|
||||||
|
password = db.Column(db.String(255), nullable=True)
|
||||||
|
registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
external_user = db.Column(db.Boolean, default=False)
|
||||||
|
external_user_id = db.Column(db.Unicode(63), unique=True, nullable=True, default=None)
|
||||||
|
last_seen = db.Column(db.DateTime, default=datetime.utcnow())
|
||||||
|
last_time_modified = db.Column(db.DateTime, default=datetime.utcnow())
|
||||||
|
jwt_exp_delta_seconds = db.Column(db.Integer, nullable=True)
|
||||||
|
acquainted = db.relationship('User',
|
||||||
|
secondary=acquaintances,
|
||||||
|
primaryjoin=(acquaintances.c.me_id == id),
|
||||||
|
secondaryjoin=(acquaintances.c.acquaintance_id == id),
|
||||||
|
backref=db.backref('acquaintances', lazy='dynamic'),
|
||||||
|
lazy='dynamic')
|
||||||
|
followed = db.relationship('User',
|
||||||
|
secondary=followers,
|
||||||
|
primaryjoin=(followers.c.follower_id == id),
|
||||||
|
secondaryjoin=(followers.c.followed_id == id),
|
||||||
|
backref=db.backref('followers', lazy='dynamic'),
|
||||||
|
lazy='dynamic')
|
||||||
|
|
||||||
|
favorite_recorders = db.relationship('Recorder', secondary=user_favorite_recorders_table)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(User, self).__init__(**kwargs)
|
||||||
|
password = kwargs.get("password", None)
|
||||||
|
external_user = kwargs.get("external_user", None)
|
||||||
|
if password is not None:
|
||||||
|
self.password = sha256_crypt.encrypt(password)
|
||||||
|
if external_user is not None:
|
||||||
|
self.external_user = external_user
|
||||||
|
|
||||||
|
@validates('email')
|
||||||
|
def validate_address(self, key, email):
|
||||||
|
assert re.match(r"[^@]+@[^@]+\.[^@]+", email), "email is invalid"
|
||||||
|
return email
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@login_manager.user_loader
|
||||||
|
def get_by_identifier(identifier):
|
||||||
|
"""
|
||||||
|
Find user by identifier, which might be the nickname or the e-mail address.
|
||||||
|
:param identifier:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return User.query.filter(or_(User.nickname == identifier,
|
||||||
|
User.email == identifier,
|
||||||
|
User.id == identifier)).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@login_manager.user_loader
|
||||||
|
def get_by_id(identifier):
|
||||||
|
"""
|
||||||
|
Find user by ID.
|
||||||
|
:param identifier:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return User.query.filter(User.id == identifier).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Return all users
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return User.query.all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_unique_nickname(nickname):
|
||||||
|
"""
|
||||||
|
Add suffix (counter) to nickname in order to get a unique nickname.
|
||||||
|
:param nickname:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if User.query.filter_by(nickname=nickname).first() is None:
|
||||||
|
return nickname
|
||||||
|
version = 2
|
||||||
|
while True:
|
||||||
|
new_nickname = nickname + str(version)
|
||||||
|
if User.query.filter_by(nickname=new_nickname).first() is None:
|
||||||
|
break
|
||||||
|
version += 1
|
||||||
|
return new_nickname
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_valid_nickname(nickname):
|
||||||
|
"""
|
||||||
|
Replaces certain characters (except a-zA-Z0-9_.) in nickname with blancs.
|
||||||
|
:param nickname:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return re.sub('[^a-zA-Z0-9_.]', '', nickname)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate(cls, **kwargs):
|
||||||
|
email = kwargs.get('email')
|
||||||
|
password = kwargs.get('password')
|
||||||
|
|
||||||
|
if not email or not password:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user = cls.query.filter_by(email=email).first()
|
||||||
|
if not user:
|
||||||
|
user = cls.query.filter_by(nickname=email).first() # be nice and allow nickname as well...
|
||||||
|
if not user or not user.verify_password(password):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
"""
|
||||||
|
Returns true if user is authenticated.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# TODO: implement correctly
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
"""
|
||||||
|
Returns true if user is active.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# TODO: implement correctly
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
"""
|
||||||
|
Returns true if user is anonymous.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# TODO: implement correctly
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_read_only(self):
|
||||||
|
"""
|
||||||
|
Returns true if user is active.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# TODO: implement correctly
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_permissions(self):
|
||||||
|
role_permissions = Config.ROLE_PERMISSION_MAPPINGS.get(self.role, set())
|
||||||
|
permissions = set(Permission.query.filter(Permission.name.in_(role_permissions)).all())
|
||||||
|
|
||||||
|
for g in self.groups:
|
||||||
|
for p in g.permissions:
|
||||||
|
permissions.add(p)
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
def has_permission(self, permission):
|
||||||
|
user_permissions = self.effective_permissions
|
||||||
|
if isinstance(permission, str):
|
||||||
|
return any([user_permission.name == permission for user_permission in user_permissions])
|
||||||
|
if isinstance(permission, Permission):
|
||||||
|
return any([user_permission.id == permission.id for user_permission in user_permissions])
|
||||||
|
if isinstance(permission, Enum):
|
||||||
|
return any([user_permission.name == str(permission.value) for user_permission in user_permissions])
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode_auth_token(auth_token):
|
||||||
|
"""
|
||||||
|
Decodes the auth token
|
||||||
|
:param auth_token:
|
||||||
|
:return: integer|string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(auth_token, app.config.get('SECRET_KEY'))
|
||||||
|
is_blacklisted_token = BlacklistToken.check_blacklist(auth_token)
|
||||||
|
if is_blacklisted_token:
|
||||||
|
return 'Token blacklisted. Please log in again.'
|
||||||
|
else:
|
||||||
|
return payload['sub']
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return 'Signature expired. Please log in again.'
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return 'Invalid token. Please log in again.'
|
||||||
|
|
||||||
|
def encode_auth_token(self):
|
||||||
|
"""
|
||||||
|
Generates the Auth Token
|
||||||
|
:return: string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
'exp': datetime.utcnow() + timedelta(days=0, hours=3, seconds=5),
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
'sub': self.id
|
||||||
|
}
|
||||||
|
return jwt.encode(
|
||||||
|
payload,
|
||||||
|
app.config.get('SECRET_KEY'),
|
||||||
|
algorithm='HS256'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
"""
|
||||||
|
SHA256 encrypts the given password and sets it on the user.
|
||||||
|
:param password:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.password = sha256_crypt.encrypt(password)
|
||||||
|
|
||||||
|
def verify_password(self, password):
|
||||||
|
"""
|
||||||
|
Verifies that the given password matches the SHA256 encrypted password stored on the user.
|
||||||
|
:param password:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.password is None:
|
||||||
|
return False
|
||||||
|
return sha256_crypt.verify(password, self.password)
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
"""
|
||||||
|
Returns the ID of the user.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
return unicode(self.id) # python 2
|
||||||
|
except NameError:
|
||||||
|
return str(self.id) # python 3
|
||||||
|
|
||||||
|
def avatar(self, size):
|
||||||
|
"""
|
||||||
|
Returns an avatar URL.
|
||||||
|
:param size:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return 'https://s.gravatar.com/avatar/%s?d=mm&s=%d' % (md5(self.email.encode('utf-8')).hexdigest(), size)
|
||||||
|
|
||||||
|
def acquaint(self, user):
|
||||||
|
"""
|
||||||
|
Adds an acquaintance to the user object.
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.is_acquainted(user):
|
||||||
|
self.acquainted.append(user)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def unacquaint(self, user):
|
||||||
|
"""
|
||||||
|
Removes the user from the list of acquaintances.
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.is_acquainted(user):
|
||||||
|
self.acquainted.remove(user)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_acquainted(self, user):
|
||||||
|
"""
|
||||||
|
Check if the provided user is an acquaintance.
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.acquainted.filter(acquaintances.c.acquaintance_id == user.id).count() > 0
|
||||||
|
|
||||||
|
def get_acquaintances(self):
|
||||||
|
"""
|
||||||
|
Returns the list of acquaintances.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return User.query.join(acquaintances, (acquaintances.c.acquaintance_id == User.id)).filter(
|
||||||
|
acquaintances.c.me_id == self.id).order_by(User.nickname.desc())
|
||||||
|
|
||||||
|
def shared_example_data_items(self):
|
||||||
|
"""
|
||||||
|
Returns a list of the shared data items.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return ExampleDataItem.query.join(acquaintances,
|
||||||
|
(acquaintances.c.acquaintance_id == ExampleDataItem.user_id)).filter(
|
||||||
|
acquaintances.c.me_id == self.id).order_by(ExampleDataItem.timestamp.desc())
|
||||||
|
|
||||||
|
def follow(self, user):
|
||||||
|
"""
|
||||||
|
Add user to list of followers.
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.is_following(user):
|
||||||
|
self.followed.append(user)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def unfollow(self, user):
|
||||||
|
"""
|
||||||
|
Remove user from the list of followers.
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.is_following(user):
|
||||||
|
self.followed.remove(user)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_following(self, user):
|
||||||
|
"""
|
||||||
|
Checks if specified user is a follower.
|
||||||
|
:param user:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.followed.filter(followers.c.followed_id == user.id).count() > 0
|
||||||
|
|
||||||
|
def followed_posts(self):
|
||||||
|
"""
|
||||||
|
Returns list of followed posts.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Post.query.join(followers, (followers.c.followed_id == Post.user_id)).filter(
|
||||||
|
followers.c.follower_id == self.id).order_by(Post.timestamp.desc())
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
# return self.__dict__
|
||||||
|
return dict(id=self.id, email=self.email, groups=[g.to_dict() for g in self.groups])
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
|
sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User %r>' % self.email
|
||||||
|
|
||||||
|
|
||||||
|
class BlacklistToken(db.Model):
|
||||||
|
"""
|
||||||
|
Token Model for storing JWT tokens
|
||||||
|
"""
|
||||||
|
__tablename__ = 'blacklist_tokens'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
token = db.Column(db.String(500), unique=True, nullable=False)
|
||||||
|
blacklisted_on = db.Column(db.DateTime, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, token):
|
||||||
|
self.token = token
|
||||||
|
self.blacklisted_on = datetime.now()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<id: token: {}'.format(self.token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_token(jwt_id):
|
||||||
|
return BlacklistToken.query.filter(BlacklistToken.token == jwt_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_blacklist(auth_token):
|
||||||
|
"""
|
||||||
|
check whether auth token has been blacklisted
|
||||||
|
:param auth_token:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
res = BlacklistToken.query.filter_by(token=str(auth_token)).first()
|
||||||
|
if res:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Group(db.Model):
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
|
||||||
|
users = db.relationship('User', secondary=user_group_table, back_populates='groups')
|
||||||
|
permissions = db.relationship('Permission', secondary=group_permission_table, back_populates='groups')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Group, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
"""
|
||||||
|
Find group by name
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Group.query.filter(Group.name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Return all groups
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Group.query.all()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(id=self.id, name=self.name)
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
|
sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(db.Model):
|
||||||
|
"""Table containing permissions associated with groups."""
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
description = db.Column(db.Unicode(511))
|
||||||
|
# read_only = db.Column(db.Boolean, default=False)
|
||||||
|
groups = db.relationship(Group, secondary=group_permission_table,
|
||||||
|
back_populates='permissions')
|
||||||
|
users = db.relationship(User, secondary=user_permission_table,
|
||||||
|
back_populates='permissions')
|
||||||
|
access_control_entry = db.relationship('AccessControlEntry', back_populates='required_permission')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
"""
|
||||||
|
Find permission by name
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Permission.query.filter(Permission.name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_names(names: list):
|
||||||
|
"""
|
||||||
|
Find permissions by their names
|
||||||
|
:param names:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if len(names) < 1:
|
||||||
|
return []
|
||||||
|
return Permission.query.filter(or_(*[Permission.name.like(name) for name in names])).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Return all permissions
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return Permission.query.all()
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(Permission.__table__, 'after_create')
|
||||||
|
def insert_initial_permissions(*args, **kwargs):
|
||||||
|
print("DB: inserting default permissions:")
|
||||||
|
for p in app.config.get("PERMISSIONS", []):
|
||||||
|
print(p)
|
||||||
|
db.session.add(Permission(name=p))
|
||||||
|
db.session.commit()
|
||||||
|
# insert_initial_groups() # call this function here again, as often (always?) permission table does not yet exist
|
||||||
|
|
||||||
|
|
||||||
|
@event.listens_for(User.__table__, 'after_create')
|
||||||
|
def insert_initial_users(*args, **kwargs):
|
||||||
|
print("DB: inserting default users:")
|
||||||
|
for u in app.config.get("USERS", []):
|
||||||
|
db.session.add(User(**u))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# The following initialization does not work as it depends on the existence of multiple tables
|
||||||
|
# This initialization has now been moved to manage.py!
|
||||||
|
"""
|
||||||
|
@event.listens_for(Group.__table__, 'after_create')
|
||||||
|
def insert_initial_groups(*args, **kwargs):
|
||||||
|
print("DB: inserting default groups:")
|
||||||
|
try:
|
||||||
|
for g in app.config.get("GROUPS", []):
|
||||||
|
print(g['name'])
|
||||||
|
g_permissions = g.pop('permissions', [])
|
||||||
|
g['permissions'] = Permission.get_by_names(g_permissions)
|
||||||
|
print(g['permissions'])
|
||||||
|
db.session.add(Group(**g))
|
||||||
|
db.session.commit()
|
||||||
|
except sqlalchemy.exc.OperationalError as e:
|
||||||
|
first_error_line = str(e).split('\n')[0]
|
||||||
|
if "no such table" not in first_error_line:
|
||||||
|
raise
|
||||||
|
print(f"Permission table probably does not exist yet: {first_error_line} - you can probably ignore this!")
|
||||||
|
"""
|
||||||
133
backend/models/virtual_command_model.py
Normal file
133
backend/models/virtual_command_model.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from sqlalchemy.orm import backref
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# virtual commands and recorder commands.
|
||||||
|
virtual_command_recorder_command_table = db.Table('virtual_command_recorder_command',
|
||||||
|
db.Column('virtual_command_id', db.Integer,
|
||||||
|
db.ForeignKey('virtual_command.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('recorder_command_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder_command.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# virtual commands and recorder commands.
|
||||||
|
virtual_command_recorder_table = db.Table('virtual_command_recorder',
|
||||||
|
db.Column('virtual_command_id', db.Integer,
|
||||||
|
db.ForeignKey('virtual_command.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('recorder_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
# probably useless!!
|
||||||
|
# This is the association table for the many-to-many relationship between
|
||||||
|
# virtual commands and recorder commands.
|
||||||
|
virtual_command_recorder_model_table = db.Table('virtual_command_recorder_model',
|
||||||
|
db.Column('virtual_command_id', db.Integer,
|
||||||
|
db.ForeignKey('virtual_command.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('recorder_model_id', db.Integer,
|
||||||
|
db.ForeignKey('recorder_model.id',
|
||||||
|
onupdate="CASCADE",
|
||||||
|
ondelete="CASCADE"),
|
||||||
|
primary_key=True))
|
||||||
|
|
||||||
|
|
||||||
|
# probably useless!!
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCommand(db.Model):
|
||||||
|
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
||||||
|
name = db.Column(db.Unicode(63), unique=True, nullable=False)
|
||||||
|
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
|
||||||
|
|
||||||
|
recorders = db.relationship('Recorder', secondary=virtual_command_recorder_table,
|
||||||
|
back_populates='virtual_commands')
|
||||||
|
|
||||||
|
recorder_commands = db.relationship('RecorderCommand', secondary=virtual_command_recorder_command_table,
|
||||||
|
back_populates='virtual_commands')
|
||||||
|
|
||||||
|
_command_default_params_json_string = db.Column(db.UnicodeText, default='')
|
||||||
|
|
||||||
|
# probably useless!!
|
||||||
|
recorder_model_commands = db.relationship('RecorderModel', secondary=virtual_command_recorder_model_table,
|
||||||
|
back_populates='virtual_commands')
|
||||||
|
# probably useless!!
|
||||||
|
|
||||||
|
# parent_virtual_command = db.relationship('VirtualCommand', back_populates='child_virtual_commands')
|
||||||
|
parent_virtual_command_id = db.Column(db.Integer, db.ForeignKey('virtual_command.id'))
|
||||||
|
child_virtual_commands = db.relationship('VirtualCommand',
|
||||||
|
backref=backref('parent_virtual_command', remote_side=[id]))
|
||||||
|
|
||||||
|
_command_order_json_string = db.Column(db.String, default='')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(VirtualCommand, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_name(name):
|
||||||
|
"""
|
||||||
|
Find group by name
|
||||||
|
:param name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return VirtualCommand.query.filter(VirtualCommand.name == name).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all():
|
||||||
|
"""
|
||||||
|
Return all groups
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return VirtualCommand.query.all()
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def command_order(self):
|
||||||
|
if self._command_order_json_string is None:
|
||||||
|
return []
|
||||||
|
return json.loads(self._command_order_json_string)
|
||||||
|
|
||||||
|
@command_order.setter
|
||||||
|
def command_order(self, ordered_list_of_commands: list):
|
||||||
|
self._command_order_json_string = json.dumps(ordered_list_of_commands)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def command_default_params(self) -> list:
|
||||||
|
return json.loads(self._command_default_params_json_string)
|
||||||
|
|
||||||
|
@command_default_params.setter
|
||||||
|
def command_default_params(self, value: list):
|
||||||
|
self._command_default_params_json_string = json.dumps(value)
|
||||||
|
|
||||||
|
def add_command_default_param(self, param_name: str, value: str):
|
||||||
|
command_default_params = json.loads(self._command_default_params_json_string)
|
||||||
|
command_default_params.append({'param_name': param_name, 'value': value})
|
||||||
|
self._command_default_params_json_string = json.dumps(command_default_params)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(id=self.id, name=self.name, description=self.description)
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return json.dumps(self.to_dict(), default=lambda o: o.__dict__,
|
||||||
|
sort_keys=True, indent=4)
|
||||||
178
backend/recorder_adapters/__init__.py
Normal file
178
backend/recorder_adapters/__init__.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
import telnetlib
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
defined_recorder_adapters = None
|
||||||
|
|
||||||
|
# monkey patching of telnet lib
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
original_read_until = telnetlib.Telnet.read_until
|
||||||
|
original_write = telnetlib.Telnet.write
|
||||||
|
|
||||||
|
|
||||||
|
def new_read_until(self, match, timeout=None):
|
||||||
|
if isinstance(match, str):
|
||||||
|
return original_read_until(self, match.encode("ascii"), timeout)
|
||||||
|
else:
|
||||||
|
return original_read_until(self, match, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def new_write(self, buffer):
|
||||||
|
if isinstance(buffer, str):
|
||||||
|
return original_write(self, buffer.encode("ascii"))
|
||||||
|
else:
|
||||||
|
return original_write(self, buffer)
|
||||||
|
|
||||||
|
|
||||||
|
telnetlib.Telnet.read_until = new_read_until
|
||||||
|
telnetlib.Telnet.write = new_write
|
||||||
|
|
||||||
|
|
||||||
|
def read_line(self, timeout=2):
|
||||||
|
return self.read_until("\n", timeout)
|
||||||
|
|
||||||
|
|
||||||
|
telnetlib.Telnet.read_line = read_line
|
||||||
|
|
||||||
|
|
||||||
|
def read_until_non_empty_line(self):
|
||||||
|
line = self.read_line()
|
||||||
|
if line is None:
|
||||||
|
return None
|
||||||
|
while len(line.rstrip()) <= 0:
|
||||||
|
line = self.read_line()
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
telnetlib.Telnet.read_until_non_empty_line = read_until_non_empty_line
|
||||||
|
|
||||||
|
|
||||||
|
def assert_string_in_output(self, string, timeout=2):
|
||||||
|
resp = self.read_until(string, timeout)
|
||||||
|
if resp is None:
|
||||||
|
return False, resp,
|
||||||
|
resp = resp.decode("ascii")
|
||||||
|
if string in resp:
|
||||||
|
return True, resp
|
||||||
|
return False, resp
|
||||||
|
|
||||||
|
|
||||||
|
telnetlib.Telnet.assert_string_in_output = assert_string_in_output
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetAdapter(ABC):
|
||||||
|
def __init__(self, address, esc_char="W"):
|
||||||
|
self.address = address
|
||||||
|
self.tn = None
|
||||||
|
self.esc_char = esc_char
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _login(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _run_cmd(self, cmd, timeout=1, auto_connect=True):
|
||||||
|
if self.tn is None and not auto_connect:
|
||||||
|
raise Exception("Not connected!")
|
||||||
|
elif self.tn is None:
|
||||||
|
self._login()
|
||||||
|
self.tn.write(cmd)
|
||||||
|
out = self.tn.read_until_non_empty_line()
|
||||||
|
res = out
|
||||||
|
while out is not None and out != "":
|
||||||
|
out = self.tn.read_until_non_empty_line()
|
||||||
|
print(out)
|
||||||
|
res += out
|
||||||
|
return res
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_response_str(tn_response):
|
||||||
|
if isinstance(tn_response, bytes):
|
||||||
|
return str(tn_response.decode("ascii").rstrip())
|
||||||
|
else:
|
||||||
|
return str(tn_response).rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
class RecorderAdapter:
|
||||||
|
def __init__(self, address: str, user: str, password: str):
|
||||||
|
self.address = address
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_recorder_params(cls) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _get_name(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _get_version(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_recording(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_recording_status(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_defined_recorder_adapters() -> list:
|
||||||
|
rec_adapters_module = importlib.import_module(".recorder_adapters", package='backend')
|
||||||
|
rec_adapter_class = getattr(rec_adapters_module, "RecorderAdapter") # needed, otherwise subclass check may fail
|
||||||
|
models = []
|
||||||
|
found_packages = list(pkgutil.iter_modules(rec_adapters_module.__path__))
|
||||||
|
for f_p in found_packages:
|
||||||
|
importer = f_p[0]
|
||||||
|
rec_model_module = importer.find_module(f_p[1]).load_module(f_p[1])
|
||||||
|
rec_model = {'id': f_p[1], 'name': f_p[1], 'commands': {}, 'path': rec_model_module.__file__}
|
||||||
|
if hasattr(rec_model_module, 'RECORDER_MODEL_NAME'):
|
||||||
|
rec_model['name'] = rec_model_module.RECORDER_MODEL_NAME
|
||||||
|
if hasattr(rec_model_module, 'REQUIRES_USER'):
|
||||||
|
rec_model['requires_user'] = rec_model_module.REQUIRES_USER
|
||||||
|
if hasattr(rec_model_module, 'REQUIRES_PW'):
|
||||||
|
rec_model['requires_password'] = rec_model_module.REQUIRES_PW
|
||||||
|
for name, obj in inspect.getmembers(rec_model_module, inspect.isclass):
|
||||||
|
if issubclass(obj, rec_adapter_class) and name != "RecorderAdapter":
|
||||||
|
rec_model['id'] = rec_model['id'] + "." + obj.__name__
|
||||||
|
rec_model['class'] = obj
|
||||||
|
commands = {}
|
||||||
|
for method_name, method in inspect.getmembers(obj, predicate=inspect.isfunction):
|
||||||
|
if len(method_name) > 0 and "_" == method_name[0]:
|
||||||
|
continue
|
||||||
|
signature = inspect.signature(method)
|
||||||
|
parameters = {}
|
||||||
|
for params in signature.parameters:
|
||||||
|
if params == "self":
|
||||||
|
continue
|
||||||
|
param_type = signature.parameters[params].annotation.__name__
|
||||||
|
param_type = "_unknown_type" if param_type == "_empty" else param_type
|
||||||
|
parameters[signature.parameters[params].name] = param_type
|
||||||
|
if len(parameters) <= 0:
|
||||||
|
parameters = None
|
||||||
|
commands[method_name] = parameters
|
||||||
|
rec_model["commands"] = commands
|
||||||
|
models.append(rec_model)
|
||||||
|
return models
|
||||||
|
|
||||||
|
|
||||||
|
def get_recorder_adapter_by_id(id: str, **kwargs):
|
||||||
|
global defined_recorder_adapters
|
||||||
|
if defined_recorder_adapters is None:
|
||||||
|
defined_recorder_adapters = get_defined_recorder_adapters()
|
||||||
|
for rec_adapter in defined_recorder_adapters:
|
||||||
|
if id in rec_adapter.get('id', '').split("."):
|
||||||
|
return rec_adapter['class'](**kwargs)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(get_defined_recorder_adapters())
|
||||||
|
get_recorder_adapter_by_id('SMP35x', address="172.22.246.207", password="123mzsmp")
|
||||||
|
exit()
|
||||||
164
backend/recorder_adapters/epiphan_base.py
Normal file
164
backend/recorder_adapters/epiphan_base.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
"""
|
||||||
|
This is the recorder adapter implementation for Epiphan Recorders. The following Epiphan recorder models are supported:
|
||||||
|
- LectureRecorder X2
|
||||||
|
- LectureRecorder
|
||||||
|
- VGADVI Recorder
|
||||||
|
- DVI Broadcaster DL
|
||||||
|
- DVIRecorderDL
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
from backend import LrcException
|
||||||
|
from backend.recorder_adapters import RecorderAdapter
|
||||||
|
|
||||||
|
# HOST = "localhost"
|
||||||
|
from backend.tools.exception_decorator import exception_decorator
|
||||||
|
|
||||||
|
RECORDER_MODEL_NAME = "Epiphan Recorder Adapter (for: LectureRecorder X2, LectureRecorder, VGADVI Recorder, " \
|
||||||
|
"DVI Broadcaster DL and DVIRecorderDL)"
|
||||||
|
|
||||||
|
BASE_URL = "http://172.23.8.102" # Audimax SMP 351
|
||||||
|
|
||||||
|
USER = "admin"
|
||||||
|
PW = "lrgrashof+-"
|
||||||
|
|
||||||
|
|
||||||
|
class Epiphan(RecorderAdapter):
|
||||||
|
def __init__(self, address: str, user: str, password: str, firmware_version: str = "", **kwargs):
|
||||||
|
if not address.startswith('http'):
|
||||||
|
address = 'http://' + address
|
||||||
|
super().__init__(address, user, password)
|
||||||
|
self.firmware_version = firmware_version
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.auth = HTTPBasicAuth(self.user, self.password)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_recorder_params(cls) -> dict:
|
||||||
|
return {'_requires_user': True,
|
||||||
|
'_requires_password': True}
|
||||||
|
|
||||||
|
def _get_name(self):
|
||||||
|
return RECORDER_MODEL_NAME
|
||||||
|
|
||||||
|
def _get_version(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@exception_decorator(ConnectionError)
|
||||||
|
def get_recording_status(self) -> dict:
|
||||||
|
res = self.session.get(self.address + "/admin/ajax/recorder_status.cgi")
|
||||||
|
if res.ok:
|
||||||
|
return res.json()
|
||||||
|
raise LrcException(res.text, res.status_code)
|
||||||
|
|
||||||
|
@exception_decorator(ConnectionError)
|
||||||
|
def get_sysinfo(self) -> dict:
|
||||||
|
res = self.session.get(self.address + "/ajax/sysinfo.cgi")
|
||||||
|
if res.ok:
|
||||||
|
return res.json()
|
||||||
|
raise LrcException(res.text, res.status_code)
|
||||||
|
|
||||||
|
def is_recording(self) -> bool:
|
||||||
|
state = self.get_recording_status().get('state', None)
|
||||||
|
return state == "up"
|
||||||
|
|
||||||
|
def get_recording_time(self):
|
||||||
|
"""
|
||||||
|
Returns recording time in seconds. Also returns 0 if not recording.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.get_recording_status().get('seconds', None)
|
||||||
|
|
||||||
|
def start_recording(self):
|
||||||
|
res = self.session.get(self.address + "/admin/ajax/start_recorder.cgi")
|
||||||
|
if not res.ok:
|
||||||
|
raise LrcException(res.text, res.status_code)
|
||||||
|
time.sleep(2) # just a little bit of waiting time -> it takes a bit for the Epiphan to update its state
|
||||||
|
|
||||||
|
def stop_recording(self):
|
||||||
|
res = self.session.get(self.address + "/admin/ajax/stop_recorder.cgi")
|
||||||
|
if not res.ok:
|
||||||
|
raise LrcException(res.text, res.status_code)
|
||||||
|
time.sleep(4) # just a little bit of waiting time -> it takes a bit for the Epiphan to update its state
|
||||||
|
|
||||||
|
def get_ip_address(self):
|
||||||
|
try:
|
||||||
|
return self.get_sysinfo().get("system").get("network").get("interfaces")[0].get('ipaddr', None)
|
||||||
|
except Exception as err:
|
||||||
|
raise LrcException(str(err))
|
||||||
|
|
||||||
|
def get_disk_space(self):
|
||||||
|
try:
|
||||||
|
data = self.get_sysinfo().get("system").get("data")
|
||||||
|
return {'available': data.get("available", None), 'free': data.get("free", None),
|
||||||
|
'total': data.get("total", None), 'used': data.get("total", 0) - data.get("available", 0)}
|
||||||
|
except Exception as err:
|
||||||
|
raise LrcException(str(err))
|
||||||
|
|
||||||
|
def get_video_inputs(self) -> list:
|
||||||
|
ret = []
|
||||||
|
try:
|
||||||
|
video = self.get_sysinfo().get("inputs").get("video")
|
||||||
|
for v in video:
|
||||||
|
ret.append(
|
||||||
|
{'id': v.get('id', None), 'name': v.get('name', None), 'resolution': v.get('resolution', None)})
|
||||||
|
return ret
|
||||||
|
except Exception as err:
|
||||||
|
raise LrcException(str(err))
|
||||||
|
|
||||||
|
def get_hardware_revision(self):
|
||||||
|
try:
|
||||||
|
return self.get_sysinfo().get("system").get("firmware")
|
||||||
|
except Exception as err:
|
||||||
|
raise LrcException(str(err))
|
||||||
|
|
||||||
|
def get_system_time(self):
|
||||||
|
try:
|
||||||
|
time_stamp = self.get_sysinfo().get("time")
|
||||||
|
return {'unix_time_stamp': time_stamp,
|
||||||
|
'date_time_utc': datetime.utcfromtimestamp(time_stamp).strftime('%Y-%m-%dT%H:%M:%SZ')}
|
||||||
|
except Exception as err:
|
||||||
|
raise LrcException(str(err))
|
||||||
|
|
||||||
|
def get_screenshot(self):
|
||||||
|
ret = self.session.get(self.address + "/admin/grab_frame.cgi?size=256x192&device=DAV93133.vga&_t=1573471990578",
|
||||||
|
stream=True)
|
||||||
|
|
||||||
|
print(ret)
|
||||||
|
pprint(ret.headers)
|
||||||
|
with open('out.jpg', 'wb') as out_file:
|
||||||
|
shutil.copyfileobj(ret.raw, out_file)
|
||||||
|
del ret
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
e = Epiphan(BASE_URL, USER, PW)
|
||||||
|
try:
|
||||||
|
# print(e.is_recording())
|
||||||
|
"""
|
||||||
|
if e.is_recording():
|
||||||
|
e.stop_recording()
|
||||||
|
else:
|
||||||
|
e.start_recording()
|
||||||
|
print(e.is_recording())
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
print(e.get_ip_address())
|
||||||
|
print(e.get_disk_space())
|
||||||
|
print(e.get_video_inputs())
|
||||||
|
print(e.get_hardware_revision())
|
||||||
|
print(e.get_system_time())
|
||||||
|
"""
|
||||||
|
|
||||||
|
e.get_screenshot()
|
||||||
|
|
||||||
|
except LrcException as e:
|
||||||
|
print(e)
|
||||||
846
backend/recorder_adapters/extron_smp.py
Normal file
846
backend/recorder_adapters/extron_smp.py
Normal file
@@ -0,0 +1,846 @@
|
|||||||
|
"""
|
||||||
|
Recorder Adapter for SMP
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from backend import LrcException
|
||||||
|
from backend.recorder_adapters import telnetlib, TelnetAdapter, RecorderAdapter
|
||||||
|
from backend.tools.exception_decorator import exception_decorator
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.recorder_adapters.extron_smp")
|
||||||
|
|
||||||
|
RECORDER_MODEL_NAME = "Recorder Adapter for SMP 351 and 352"
|
||||||
|
VERSION = "0.9.0"
|
||||||
|
REQUIRES_USER = False
|
||||||
|
REQUIRES_PW = True
|
||||||
|
|
||||||
|
# HOST = "localhost"
|
||||||
|
# HOST = "129.13.51.102" # Audimax SMP 351
|
||||||
|
# HOST = "129.13.51.106" # Tulla SMP 351
|
||||||
|
HOST = "172.22.246.207" # Test SMP MZ
|
||||||
|
# HOST = "129.13.51.109" # Hertz
|
||||||
|
|
||||||
|
USER = "admin"
|
||||||
|
PW = "123mzsmp"
|
||||||
|
# PW = "audimaxsmp"
|
||||||
|
# PW = "smphertz"
|
||||||
|
|
||||||
|
|
||||||
|
class SMP35x(TelnetAdapter, RecorderAdapter):
|
||||||
|
def __init__(self, address, password, auto_login=True, **kwargs):
|
||||||
|
RecorderAdapter.__init__(self, address, "", password)
|
||||||
|
TelnetAdapter.__init__(self, address)
|
||||||
|
if auto_login:
|
||||||
|
self._login()
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
logger.debug("Connecting to {} ...".format(self.address))
|
||||||
|
try:
|
||||||
|
self.tn = telnetlib.Telnet(self.address)
|
||||||
|
except TimeoutError as e:
|
||||||
|
raise LrcException(str(e))
|
||||||
|
except ConnectionRefusedError as e:
|
||||||
|
raise LrcException(str(e))
|
||||||
|
self.tn.read_until("\r\nPassword:")
|
||||||
|
# password = getpass.getpass()
|
||||||
|
password = self.password
|
||||||
|
self.tn.write(password + "\n\r")
|
||||||
|
|
||||||
|
out = self.tn.assert_string_in_output("Login Administrator")
|
||||||
|
# print(out)
|
||||||
|
if not out[0]:
|
||||||
|
# print(out[1])
|
||||||
|
if "Password:" in out[1]:
|
||||||
|
# TODO: loop until logged in...
|
||||||
|
logger.warning("Could not login (as admin) with given password! {}".format(self.address))
|
||||||
|
logger.debug("re-enter password")
|
||||||
|
self.tn.write(self.password + "\n\r")
|
||||||
|
# print(self.tn.assert_string_in_output("Login Administrator"))
|
||||||
|
self.tn = None
|
||||||
|
logger.error("Could definitely not login (as admin) with given password! {}".format(self.address))
|
||||||
|
raise LrcException("Could not login as administrator with given pw!")
|
||||||
|
# print("OK, we have admin rights!")
|
||||||
|
|
||||||
|
def _get_name(self):
|
||||||
|
return RECORDER_MODEL_NAME
|
||||||
|
|
||||||
|
def _get_version(self):
|
||||||
|
return VERSION
|
||||||
|
|
||||||
|
def get_version(self, include_build=False, verbose_info=False):
|
||||||
|
if verbose_info:
|
||||||
|
self.tn.write("0Q")
|
||||||
|
else:
|
||||||
|
if include_build:
|
||||||
|
self.tn.write("*Q\n")
|
||||||
|
else:
|
||||||
|
self.tn.write("1Q\n")
|
||||||
|
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_bootstrap_version(self):
|
||||||
|
self.tn.write("2Q")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_factory_firmware_version(self):
|
||||||
|
self.tn.write("3Q")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_updated_firmware_version(self):
|
||||||
|
self.tn.write("4Q")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_part_number(self):
|
||||||
|
self.tn.write("N")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_model_name(self):
|
||||||
|
self.tn.write("1I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_model_description(self):
|
||||||
|
self.tn.write("2I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_system_memory_usage(self):
|
||||||
|
self.tn.write("3I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_number_of_connected_users(self):
|
||||||
|
self.tn.write("10I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_system_processer_usage(self):
|
||||||
|
self.tn.write("11I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_system_processor_idle(self):
|
||||||
|
self.tn.write("12I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_eth0_link_status(self):
|
||||||
|
self.tn.write("13I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_file_transfer_config(self):
|
||||||
|
self.tn.write("38I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_active_alarms(self):
|
||||||
|
self.tn.write("39I")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_unit_name(self, name: str):
|
||||||
|
# TODO: check name (must comply with internet host name standards)
|
||||||
|
self.tn.write(self.esc_char + name + "CN\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reset_unit_name(self):
|
||||||
|
self.tn.write(self.esc_char + " CN\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_unit_name(self):
|
||||||
|
self.tn.write(self.esc_char + "CN\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_telnet_connections(self):
|
||||||
|
self.tn.write(self.esc_char + "CC\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_verbose_mode(self, mode: int):
|
||||||
|
"""
|
||||||
|
Mode:
|
||||||
|
0=clear/none (default for telnet connections
|
||||||
|
1=verbose mode (default for USB and RS-232 host control)
|
||||||
|
2=tagged responses for queries
|
||||||
|
3=verbose mode and tagged responses for queries
|
||||||
|
:param mode:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if mode not in range(4):
|
||||||
|
raise LrcException("Only values from 0 to 3 are allowed!")
|
||||||
|
self.tn.write(self.esc_char + str(mode) + "CV\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_verbose_mode(self):
|
||||||
|
self.tn.write(self.esc_char + "CV\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def save_configuration(self, mode: int = 2):
|
||||||
|
"""
|
||||||
|
Mode:
|
||||||
|
0 = IP config
|
||||||
|
2 = Box specific parameters
|
||||||
|
:param mode:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if mode not in [0, 2]:
|
||||||
|
raise LrcException("Only values 0 and 2 are allowed!")
|
||||||
|
self.tn.write(self.esc_char + "1*{}XF\n".format(mode))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def restore_configuration(self, mode: int = 2):
|
||||||
|
"""
|
||||||
|
Mode:
|
||||||
|
0 = IP config
|
||||||
|
2 = Box specific parameters
|
||||||
|
:param mode:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if mode not in [0, 2]:
|
||||||
|
raise LrcException("Only values 0 and 2 are allowed!")
|
||||||
|
self.tn.write(self.esc_char + "0*{}XF\n".format(mode))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reboot(self):
|
||||||
|
self.tn.write(self.esc_char + "1BOOT\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def restart_network(self):
|
||||||
|
self.tn.write(self.esc_char + "2BOOT\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reset_flash(self):
|
||||||
|
"""
|
||||||
|
Reset flash memory (excludes recording files).
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.tn.write(self.esc_char + "ZFFF\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def system_reset(self):
|
||||||
|
"""
|
||||||
|
Resets device to default and deletes recorded files
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.tn.write(self.esc_char + "ZXXX\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reset_settings_and_delete_all_files(self):
|
||||||
|
"""
|
||||||
|
Reset to default except IP address, delete all user and recorded files
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.tn.write(self.esc_char + "ZY\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def absolute_reset(self):
|
||||||
|
"""
|
||||||
|
Same as System Reset, plus returns the IP address and subnet mask to defaults.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.tn.write(self.esc_char + "ZQQQ\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_front_panel_lock(self, mode: int):
|
||||||
|
"""
|
||||||
|
0=Off
|
||||||
|
1=complete lockout (no front panel control)
|
||||||
|
2=menu lockout
|
||||||
|
3=recording controls
|
||||||
|
:param mode: Execute mode int code
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if mode not in range(4):
|
||||||
|
raise LrcException("Only values from 0 to 3 are allowed!")
|
||||||
|
self.tn.write(str(mode) + "X\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_front_panel_lock(self):
|
||||||
|
"""
|
||||||
|
View executive mode.
|
||||||
|
0=Off
|
||||||
|
1=complete lockout (no front panel control)
|
||||||
|
2=menu lockout
|
||||||
|
3=recording controls
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.tn.write("X\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
"""
|
||||||
|
A lot of stuff related to network settings (ports of services, SNMP, IP, DHCP, etc.)
|
||||||
|
Only some stuff will be implemented here!
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
def get_date_time(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_time_zone(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_dhcp_mode(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_network_settings(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_ip_address(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_mac_address(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_subnet_mask(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_gateway_ip(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_dns_server_ip(self):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
RS-232 / serial port related stuff not implemented.
|
||||||
|
Password and security related stuff not implemented.
|
||||||
|
File related stuff not implemented. (-> use sftp)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_input(self, input_num: int, channel_num: int):
|
||||||
|
"""
|
||||||
|
Switches input # (1 to 5) to output channel (1=A [input 1 and 2], 2=B [input 3, 4 and 5])
|
||||||
|
:param input_num:
|
||||||
|
:param channel_num:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if input_num not in range(1, 6):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 5!")
|
||||||
|
if channel_num not in range(1, 3):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 2!")
|
||||||
|
self.tn.write("{}*{}!\n".format(input_num, channel_num))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input(self, channel_num: int):
|
||||||
|
if channel_num not in range(1, 2):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 2!")
|
||||||
|
self.tn.write("{}!\n".format(channel_num))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_input_format(self, input_num: int, input_format: int):
|
||||||
|
"""
|
||||||
|
Sets the input to the format, where the input_format parameter may be:
|
||||||
|
1 = YUVp / HDTV (default)
|
||||||
|
2 = YUVi
|
||||||
|
3 = Composite
|
||||||
|
:param input_num:
|
||||||
|
:param input_format:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if input_num not in range(1, 6):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 5!")
|
||||||
|
if input_format not in range(1, 4):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 3!")
|
||||||
|
self.tn.write("{}*{}\\\n".format(input_num, input_format))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input_format(self, input_num: int):
|
||||||
|
if input_num not in range(1, 6):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 5!")
|
||||||
|
self.tn.write("{}\\\n".format(input_num))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_input_name(self, input_num: int, input_name: str):
|
||||||
|
if input_num not in range(1, 6):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 5!")
|
||||||
|
if len(input_name) > 16:
|
||||||
|
raise LrcException("input_name must be no longer than 16 chars")
|
||||||
|
try:
|
||||||
|
input_name.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise LrcException("input_name must only contain ascii characters")
|
||||||
|
self.tn.write("{}{},{}NI\n".format(self.esc_char, input_num, input_name))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input_name(self, input_num: int):
|
||||||
|
if input_num not in range(1, 6):
|
||||||
|
raise LrcException("input_num must be a value between 1 and 5!")
|
||||||
|
self.tn.write("{}{}NI\n".format(self.esc_char, input_num))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input_selection_per_channel(self):
|
||||||
|
self.tn.write("32I\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input configuration part skipped
|
||||||
|
"""
|
||||||
|
|
||||||
|
def stop_recording(self):
|
||||||
|
self.tn.write("{}Y0RCDR\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def start_recording(self):
|
||||||
|
self.tn.write("{}Y1RCDR\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def pause_recording(self):
|
||||||
|
self.tn.write("{}Y2RCDR\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
@exception_decorator(ConnectionError)
|
||||||
|
def get_recording_status(self):
|
||||||
|
"""
|
||||||
|
Status may be one of:
|
||||||
|
0=stop
|
||||||
|
1=record
|
||||||
|
2=pause
|
||||||
|
:return: status
|
||||||
|
"""
|
||||||
|
self.tn.write("{}YRCDR\n".format(self.esc_char))
|
||||||
|
return int(TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line()))
|
||||||
|
|
||||||
|
def is_recording(self) -> bool:
|
||||||
|
return self.get_recording_status() == 1
|
||||||
|
|
||||||
|
def extent_recording_time(self, extension_time: int):
|
||||||
|
"""
|
||||||
|
Extends a scheduled recording by extension_time minutes
|
||||||
|
:param extension_time: must be an int from 0 to 99
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if extension_time not in range(0, 100):
|
||||||
|
raise LrcException("extension_time must be a value between 0 and 99!")
|
||||||
|
self.tn.write("{}E{}RCDR\n".format(self.esc_char, extension_time))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def add_chapter_marker(self):
|
||||||
|
self.tn.write("{}BRCDR\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def swap_channel_positions(self):
|
||||||
|
self.tn.write("%\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_recording_status_text(self):
|
||||||
|
self.tn.write("I\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_elapsed_recording_time(self):
|
||||||
|
self.tn.write("35I\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_remaining_recording_time(self):
|
||||||
|
self.tn.write("36I\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_recording_destination(self):
|
||||||
|
self.tn.write("37I\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
"""
|
||||||
|
Metadata part skipped
|
||||||
|
"""
|
||||||
|
|
||||||
|
def recall_user_preset(self, channel_number: int, preset_number: int):
|
||||||
|
if channel_number not in range(1, 3):
|
||||||
|
raise LrcException("channel_number must be a value between 1 and 2!")
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("1*{}*{}.\n".format(channel_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def save_user_preset(self, channel_number: int, preset_number: int):
|
||||||
|
if channel_number not in range(1, 3):
|
||||||
|
raise LrcException("channel_number must be a value between 1 and 2!")
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("1*{}*{},\n".format(channel_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_user_preset_name(self, preset_number: int, preset_name: str):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
if len(preset_name) > 16:
|
||||||
|
raise LrcException("preset_name must be no longer than 16 chars")
|
||||||
|
try:
|
||||||
|
preset_name.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise LrcException("preset_name must only contain ascii characters")
|
||||||
|
self.tn.write("{}1*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_user_preset_name(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}1*{}PNAM\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_user_presets(self, input_number: int):
|
||||||
|
if input_number not in range(1, 6):
|
||||||
|
raise LrcException("input_number must be a value between 1 and 5!")
|
||||||
|
self.tn.write("52*{}#\n".format(input_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
# Input Presets
|
||||||
|
def recall_input_preset(self, channel_number: int, preset_number: int):
|
||||||
|
if channel_number not in range(1, 3):
|
||||||
|
raise LrcException("channel_number must be a value between 1 and 2!")
|
||||||
|
if preset_number not in range(1, 129):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 128!")
|
||||||
|
self.tn.write("2*{}*{}.\n".format(channel_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def save_input_preset(self, channel_number: int, preset_number: int):
|
||||||
|
if channel_number not in range(1, 3):
|
||||||
|
raise LrcException("channel_number must be a value between 1 and 2!")
|
||||||
|
if preset_number not in range(1, 129):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 128!")
|
||||||
|
self.tn.write("1*{}*{},\n".format(channel_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_input_preset_name(self, preset_number: int, preset_name: str):
|
||||||
|
if preset_number not in range(1, 129):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 128!")
|
||||||
|
if len(preset_name) > 16:
|
||||||
|
raise LrcException("preset_name must be no longer than 16 chars")
|
||||||
|
try:
|
||||||
|
preset_name.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise LrcException("preset_name must only contain ascii characters")
|
||||||
|
self.tn.write("{}2*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input_preset_name(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 129):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 128!")
|
||||||
|
self.tn.write("{}2*{}PNAM\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def delete_input_preset(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 129):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 128!")
|
||||||
|
self.tn.write("{}X2*{}PRST\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input_presets(self):
|
||||||
|
self.tn.write("51#\n")
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
# Streaming Presets
|
||||||
|
def recall_streaming_preset(self, output_number: int, preset_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
3 = Confidence Stream
|
||||||
|
:param preset_number:
|
||||||
|
:param output_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 4):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 3!")
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("3*{}*{}.\n".format(output_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def save_streaming_preset(self, output_number: int, preset_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
3 = Confidence Stream
|
||||||
|
:param output_number:
|
||||||
|
:param preset_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 4):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 3!")
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("3*{}*{},\n".format(output_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_streaming_preset_name(self, preset_number: int, preset_name: str):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
if len(preset_name) > 16:
|
||||||
|
raise LrcException("preset_name must be no longer than 16 chars")
|
||||||
|
try:
|
||||||
|
preset_name.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise LrcException("preset_name must only contain ascii characters")
|
||||||
|
self.tn.write("{}3*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_streaming_preset_name(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}3*{}PNAM\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reset_streaming_preset_to_default(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}X3*{}PRST\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
# Encoder Presets
|
||||||
|
def recall_encoder_preset(self, output_number: int, preset_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
3 = Confidence Stream
|
||||||
|
:param preset_number:
|
||||||
|
:param output_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 4):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 3!")
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("4*{}*{}.\n".format(output_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def save_encoder_preset(self, output_number: int, preset_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
3 = Confidence Stream
|
||||||
|
:param preset_number:
|
||||||
|
:param output_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 4):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 3!")
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("4*{}*{},\n".format(output_number, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_encoder_preset_name(self, preset_number: int, preset_name: str):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
if len(preset_name) > 16:
|
||||||
|
raise LrcException("preset_name must be no longer than 16 chars")
|
||||||
|
try:
|
||||||
|
preset_name.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise LrcException("preset_name must only contain ascii characters")
|
||||||
|
self.tn.write("{}4*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_encoder_preset_name(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}4*{}PNAM\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reset_encoder_preset_to_default(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}X4*{}PRST\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
# Layout Presets
|
||||||
|
def save_layout_preset(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("7*{},\n".format(preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def recall_layout_preset(self, preset_number: int, include_input_selections: bool = True):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
if include_input_selections:
|
||||||
|
self.tn.write("7*{}.\n".format(preset_number))
|
||||||
|
else:
|
||||||
|
self.tn.write("8*{}.\n".format(preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_layout_preset_name(self, preset_number: int, preset_name: str):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
if len(preset_name) > 16:
|
||||||
|
raise LrcException("preset_name must be no longer than 16 chars")
|
||||||
|
try:
|
||||||
|
preset_name.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise LrcException("preset_name must only contain ascii characters")
|
||||||
|
self.tn.write("{}7*{},{}PNAM\n".format(self.esc_char, preset_number, preset_name))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_layout_preset_name(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}7*{}PNAM\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def reset_layout_preset_to_default(self, preset_number: int):
|
||||||
|
if preset_number not in range(1, 17):
|
||||||
|
raise LrcException("preset_number must be a value between 1 and 16!")
|
||||||
|
self.tn.write("{}X7*{}PRST\n".format(self.esc_char, preset_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input adjustments skipped
|
||||||
|
Picture adjustments skipped
|
||||||
|
"""
|
||||||
|
|
||||||
|
def mute_output(self, output_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
:param output_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 3):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 2!")
|
||||||
|
self.tn.write("{}*1B\n".format(output_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def unmute_output(self, output_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
:param output_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 3):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 2!")
|
||||||
|
self.tn.write("{}*0B\n".format(output_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def is_muted(self, output_number: int):
|
||||||
|
"""
|
||||||
|
Output_number:
|
||||||
|
1 = Channel A
|
||||||
|
2 = Channel B
|
||||||
|
:param output_number:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if output_number not in range(1, 3):
|
||||||
|
raise LrcException("output_number must be a value between 1 and 2!")
|
||||||
|
self.tn.write("{}B\n".format(output_number))
|
||||||
|
return int(TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())) > 0
|
||||||
|
|
||||||
|
"""
|
||||||
|
EDID skipped
|
||||||
|
Encoder settings skipped
|
||||||
|
some advanced options skipped
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_recorder_params(cls) -> dict:
|
||||||
|
return {'_requires_user': False,
|
||||||
|
'_requires_password': True}
|
||||||
|
|
||||||
|
def get_input_hdcp_status(self, input_number: int):
|
||||||
|
"""
|
||||||
|
returns:
|
||||||
|
0 = no sink / source detected
|
||||||
|
1 = sink / source detected with HDCP
|
||||||
|
2 = sink / source detected without HDCP
|
||||||
|
:param input_number: from 1 to 5
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if input_number not in range(1, 6):
|
||||||
|
raise LrcException("input_number must be a value between 1 and 6!")
|
||||||
|
self.tn.write("{}I{}HDCP\n".format(self.esc_char, input_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_input_authorization_hdcp_on(self, input_number: int):
|
||||||
|
if input_number not in range(1, 6):
|
||||||
|
raise LrcException("input_number must be a value between 1 and 6!")
|
||||||
|
self.tn.write("{}E{}*1HDCP\n".format(self.esc_char, input_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def set_input_authorization_hdcp_off(self, input_number: int):
|
||||||
|
if input_number not in range(1, 6):
|
||||||
|
raise LrcException("input_number must be a value between 1 and 6!")
|
||||||
|
self.tn.write("{}E{}*0HDCP\n".format(self.esc_char, input_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_input_authorization_hdcp_status(self, input_number: int):
|
||||||
|
if input_number not in range(1, 6):
|
||||||
|
raise LrcException("input_number must be a value between 1 and 6!")
|
||||||
|
self.tn.write("{}E{}HDCP\n".format(self.esc_char, input_number))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def enable_hdcp_notification(self):
|
||||||
|
self.tn.write("{}N1HDCP\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def disable_hdcp_notification(self):
|
||||||
|
self.tn.write("{}N0HDCP\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_hdcp_notification_status(self):
|
||||||
|
self.tn.write("{}NHDCP\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
# background image settings
|
||||||
|
def set_background_image(self, filename: str):
|
||||||
|
self.tn.write("{}{}RF\n".format(self.esc_char, filename))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def get_background_image_filename(self):
|
||||||
|
self.tn.write("{}RF\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
def mute_background_image(self):
|
||||||
|
self.tn.write("{}0RF\n".format(self.esc_char))
|
||||||
|
return TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
smp = SMP35x(HOST, PW, True)
|
||||||
|
|
||||||
|
print(smp)
|
||||||
|
print(smp.get_recording_status())
|
||||||
|
print(smp.is_recording())
|
||||||
|
print(smp.get_input_presets())
|
||||||
|
print(smp.get_input_selection_per_channel())
|
||||||
|
print(smp.get_recording_status_text())
|
||||||
|
|
||||||
|
exit()
|
||||||
|
smp._login()
|
||||||
|
print(smp.get_version(verbose_info=False))
|
||||||
|
print(smp.get_file_transfer_config())
|
||||||
|
print(smp.save_configuration())
|
||||||
|
print(smp.restore_configuration())
|
||||||
|
|
||||||
|
return
|
||||||
|
print(smp.get_bootstrap_version())
|
||||||
|
print(smp.get_part_number())
|
||||||
|
print(smp.get_model_name())
|
||||||
|
print(smp.get_model_description())
|
||||||
|
|
||||||
|
print(smp.get_system_memory_usage())
|
||||||
|
|
||||||
|
print(smp.get_file_transfer_config())
|
||||||
|
|
||||||
|
# print(smp.get_unit_name())
|
||||||
|
# print(smp.set_unit_name("mzsmp"))
|
||||||
|
# print(smp.get_unit_name())
|
||||||
|
# print(smp.reset_unit_name())
|
||||||
|
|
||||||
|
print(smp.set_front_panel_lock(0))
|
||||||
|
print(smp.get_front_panel_lock())
|
||||||
|
|
||||||
|
print(smp.get_input_name(1))
|
||||||
|
print(smp.get_input_selction_per_channel())
|
||||||
|
print(smp.get_recording_status())
|
||||||
|
print("Preset Name: " + smp.get_user_preset_name(2))
|
||||||
|
print(smp.get_user_presets(1))
|
||||||
|
print(smp.get_input_presets())
|
||||||
|
print(smp.get_layout_preset_name(2))
|
||||||
|
print(smp.get_encoder_preset_name(1))
|
||||||
|
print(smp.get_streaming_preset_name(2))
|
||||||
|
print(smp.recall_encoder_preset(3, 1))
|
||||||
|
|
||||||
|
print(smp.is_muted(2))
|
||||||
|
print(smp.mute_output(2))
|
||||||
|
print(smp.is_muted(2))
|
||||||
|
print(smp.unmute_output(2))
|
||||||
|
print(smp.is_muted(2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
61
backend/recorder_adapters/helpers.py
Normal file
61
backend/recorder_adapters/helpers.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from backend.models import Recorder, RecorderCommand, LrcException
|
||||||
|
from backend.recorder_adapters import get_recorder_adapter_by_id
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.recorder_adapters.helpers")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_recorder_command_params(recorder_command: RecorderCommand, params: dict, fail_on_missing_params=False):
|
||||||
|
if recorder_command.parameters is None or len(recorder_command.parameters) == 0:
|
||||||
|
if len(params) == 0:
|
||||||
|
logger.debug("Number (0) of parameters matching expected number of args of command signature.")
|
||||||
|
return True
|
||||||
|
logger.info("More arguments specified ({}) than expected!".format(len(params)))
|
||||||
|
return False
|
||||||
|
for p_n in recorder_command.parameters:
|
||||||
|
p_t = recorder_command.parameters[p_n]
|
||||||
|
p = params.get(p_n, None)
|
||||||
|
if p is None:
|
||||||
|
if fail_on_missing_params:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if p_t == 'int':
|
||||||
|
params[p_n] = int(p)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_function_from_recorder_command(recorder: Recorder, recorder_command: RecorderCommand,
|
||||||
|
connect_by_network_name=True):
|
||||||
|
adapter_name, function_name = recorder_command.name.split(':')
|
||||||
|
if connect_by_network_name and recorder.network_name is not None:
|
||||||
|
address = recorder.network_name
|
||||||
|
elif recorder.ip6 is not None:
|
||||||
|
address = recorder.ip6
|
||||||
|
else:
|
||||||
|
address = recorder.ip
|
||||||
|
logger.debug("Using {} to create recorder adapter to connect to {}.".format(address, recorder.name))
|
||||||
|
package_name, class_name = adapter_name.split('.')
|
||||||
|
adapter = get_recorder_adapter_by_id(class_name, address=address, user=recorder.username,
|
||||||
|
password=recorder.password, firmware_version=recorder.firmware_version)
|
||||||
|
logger.debug("Built adapter {}".format(adapter))
|
||||||
|
return getattr(adapter, function_name)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_recorder_command(recorder: Recorder, recorder_command: RecorderCommand, params: Union[dict, None] = None,
|
||||||
|
connect_by_network_name=True):
|
||||||
|
if params is None:
|
||||||
|
params = dict()
|
||||||
|
if validate_recorder_command_params(recorder_command, params):
|
||||||
|
try:
|
||||||
|
func = get_function_from_recorder_command(recorder, recorder_command, connect_by_network_name)
|
||||||
|
logger.debug(
|
||||||
|
"Executing func: {} with params: '{}'".format(func, ", ".join(": ".join(str(_)) for _ in params.items())))
|
||||||
|
out = func(**params)
|
||||||
|
return True, out
|
||||||
|
except LrcException as e:
|
||||||
|
return False, str(e)
|
||||||
|
else:
|
||||||
|
logger.info("Could not validate given parameters!")
|
||||||
|
return False
|
||||||
78
backend/serve_frontend.py
Normal file
78
backend/serve_frontend.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import flask
|
||||||
|
from flask.json import dump
|
||||||
|
from jose import jwt, jwk
|
||||||
|
import os
|
||||||
|
from flask import render_template, send_from_directory, Blueprint, jsonify, url_for
|
||||||
|
from flask_pyoidc.user_session import UserSession
|
||||||
|
|
||||||
|
from backend import app
|
||||||
|
from backend.auth import oidc_auth
|
||||||
|
from backend.auth.oidc_config import PROVIDER_NAME
|
||||||
|
|
||||||
|
fe_path = os.path.abspath(os.path.join(app.root_path, os.pardir, os.pardir, "frontend", "dist"))
|
||||||
|
if not os.path.exists(fe_path) or not os.path.exists(os.path.join(fe_path, "index.html")):
|
||||||
|
app.logger.critical(
|
||||||
|
"Frontend path and/or index.html does not exist! Please build frontend before continuing! "
|
||||||
|
"You might want to go to ../frontend and continue from there.")
|
||||||
|
print("FATAL: Frontend path wrong or index.html missing -> EXITING!")
|
||||||
|
exit()
|
||||||
|
fe_bp = Blueprint('frontend', __name__, url_prefix='/', template_folder=os.path.join(fe_path, ""))
|
||||||
|
|
||||||
|
|
||||||
|
@fe_bp.route('/js/<path:path>')
|
||||||
|
def send_js(path):
|
||||||
|
return send_from_directory(os.path.join(fe_path, "js"), path)
|
||||||
|
|
||||||
|
|
||||||
|
@fe_bp.route('/css/<path:path>')
|
||||||
|
def send_css(path):
|
||||||
|
return send_from_directory(os.path.join(fe_path, "css"), path)
|
||||||
|
|
||||||
|
|
||||||
|
@fe_bp.route('/img/<path:path>')
|
||||||
|
def send_img(path):
|
||||||
|
return send_from_directory(os.path.join(fe_path, "img"), path)
|
||||||
|
|
||||||
|
|
||||||
|
@fe_bp.route('/test')
|
||||||
|
@oidc_auth.oidc_auth(provider_name=PROVIDER_NAME)
|
||||||
|
def test_oidc():
|
||||||
|
user_session = UserSession(flask.session)
|
||||||
|
access_token = user_session.access_token
|
||||||
|
token_claim = jwt.get_unverified_claims(access_token)
|
||||||
|
token_header = jwt.get_unverified_header(access_token)
|
||||||
|
return jsonify(id_token=flask.session['id_token'], access_token=flask.session['access_token'],
|
||||||
|
userinfo=flask.session['userinfo'],
|
||||||
|
token_claim=token_claim,
|
||||||
|
token_header=token_header)
|
||||||
|
|
||||||
|
|
||||||
|
def has_no_empty_params(rule):
|
||||||
|
defaults = rule.defaults if rule.defaults is not None else ()
|
||||||
|
arguments = rule.arguments if rule.arguments is not None else ()
|
||||||
|
return len(defaults) >= len(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
@fe_bp.route("/site-map")
|
||||||
|
def site_map():
|
||||||
|
print("# serving site-map!!")
|
||||||
|
links = []
|
||||||
|
for rule in app.url_map.iter_rules():
|
||||||
|
# Filter out rules we can't navigate to in a browser
|
||||||
|
# and rules that require parameters
|
||||||
|
if has_no_empty_params(rule):
|
||||||
|
# if "GET" in rule.methods and has_no_empty_params(rule):
|
||||||
|
url = url_for(rule.endpoint, **(rule.defaults or {}))
|
||||||
|
links.append((url, rule.endpoint))
|
||||||
|
# links is now a list of url, endpoint tuples
|
||||||
|
# dump(links)
|
||||||
|
return jsonify(links)
|
||||||
|
|
||||||
|
|
||||||
|
@fe_bp.route('/', defaults={'path': ''})
|
||||||
|
@fe_bp.route('/<path:path>')
|
||||||
|
def catch_all(path):
|
||||||
|
return render_template("index.html")
|
||||||
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
20
backend/tests/base.py
Normal file
20
backend/tests/base.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
|
||||||
|
from flask_testing import TestCase
|
||||||
|
from backend import app, db
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTestCase(TestCase):
|
||||||
|
""" Base Tests """
|
||||||
|
|
||||||
|
def create_app(self):
|
||||||
|
app.config.from_object('backend.config.Config')
|
||||||
|
return app
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
db.create_all()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.session.remove()
|
||||||
|
db.drop_all()
|
||||||
65
backend/tests/extron_smp_testing.py
Normal file
65
backend/tests/extron_smp_testing.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import getpass
|
||||||
|
import sys
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from backend.recorder_adapters import telnetlib
|
||||||
|
|
||||||
|
# HOST = "localhost"
|
||||||
|
# HOST = "129.13.51.102" # Audimax SMP 351
|
||||||
|
# HOST = "129.13.51.106" # Tulla SMP 351
|
||||||
|
HOST = "172.22.246.207" # Test SMP MZ
|
||||||
|
|
||||||
|
user = "admin"
|
||||||
|
pw = "123mzsmp"
|
||||||
|
|
||||||
|
|
||||||
|
def print_tn(tn_response):
|
||||||
|
if isinstance(tn_response, bytes):
|
||||||
|
print(tn_response.decode("ascii").rstrip())
|
||||||
|
else:
|
||||||
|
print(str(tn_response).rstrip())
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(tn, cmd, timeout=1):
|
||||||
|
tn.write(cmd)
|
||||||
|
out = tn.read_until_non_empty_line()
|
||||||
|
res = out
|
||||||
|
while out is not None and out != "":
|
||||||
|
out = tn.read_until_non_empty_line()
|
||||||
|
print(out)
|
||||||
|
res += out
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
tn = telnetlib.Telnet(HOST)
|
||||||
|
tn.read_until("\r\nPassword:")
|
||||||
|
# password = getpass.getpass()
|
||||||
|
password = pw
|
||||||
|
tn.write(password + "\n\r")
|
||||||
|
|
||||||
|
if not tn.assert_string_in_output("Login Administrator")[0]:
|
||||||
|
print("WRONG (admin) password!! Exiting!")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print("OK, we have admin rights!")
|
||||||
|
|
||||||
|
tn.write("1Q\n")
|
||||||
|
|
||||||
|
# print_tn(read_line(tn))
|
||||||
|
print_tn(tn.read_until_non_empty_line())
|
||||||
|
print("test")
|
||||||
|
|
||||||
|
|
||||||
|
# print(run_cmd(tn, "I\n"))
|
||||||
|
# run_cmd(tn, "X1CERT\n")
|
||||||
|
# tn.write(chr(27)+"X1CERT")
|
||||||
|
# tn.write("WX1CERT\n")
|
||||||
|
# tn.write(chr(27)+"1BOOT")
|
||||||
|
# tn.write("W1BOOT\n")
|
||||||
|
# print_tn(tn.read_until_non_empty_line())
|
||||||
|
# tn.write("I\n")
|
||||||
|
# print_tn(tn.read_some())
|
||||||
|
|
||||||
|
# tn.write("exit\n\r".encode("ascii"))
|
||||||
|
|
||||||
|
# print_tn(tn.read_eager())
|
||||||
|
|
||||||
49
backend/tests/test__config.py
Normal file
49
backend/tests/test__config.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from flask_testing import TestCase
|
||||||
|
|
||||||
|
from backend import app
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.join(os.path.abspath(app.root_path), os.pardir))
|
||||||
|
|
||||||
|
|
||||||
|
class TestDevelopmentConfig(TestCase):
|
||||||
|
def create_app(self):
|
||||||
|
app.config.from_object('backend.config.DevelopmentConfig')
|
||||||
|
return app
|
||||||
|
|
||||||
|
def test_app_is_development(self):
|
||||||
|
self.assertFalse(app.config['SECRET_KEY'] is 'you-will-never-guess')
|
||||||
|
self.assertTrue(app.config['DEBUG'] is True)
|
||||||
|
self.assertFalse(current_app is None)
|
||||||
|
self.assertTrue(
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'app.db_debug')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTestingConfig(TestCase):
|
||||||
|
def create_app(self):
|
||||||
|
app.config.from_object('backend.config.TestingConfig')
|
||||||
|
return app
|
||||||
|
|
||||||
|
def test_app_is_testing(self):
|
||||||
|
self.assertFalse(app.config['SECRET_KEY'] is 'you-will-never-guess')
|
||||||
|
self.assertTrue(app.config['DEBUG'])
|
||||||
|
self.assertTrue(
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'app.db_test')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProductionConfig(TestCase):
|
||||||
|
def create_app(self):
|
||||||
|
app.config.from_object('backend.config.Config')
|
||||||
|
return app
|
||||||
|
|
||||||
|
def test_app_is_production(self):
|
||||||
|
self.assertTrue(app.config['DEBUG'] is False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
287
backend/tests/test_auth.py
Normal file
287
backend/tests/test_auth.py
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
from backend.models.user_model import User, BlacklistToken
|
||||||
|
from backend.tests.base import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
def register_user(self, email, password):
|
||||||
|
return self.client.post(
|
||||||
|
'/auth/register',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email=email,
|
||||||
|
password=password
|
||||||
|
)),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthBlueprint(BaseTestCase):
|
||||||
|
def test_registration(self):
|
||||||
|
""" Test for user registration """
|
||||||
|
with self.client:
|
||||||
|
response = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['message'] == 'Successfully registered.')
|
||||||
|
self.assertTrue(data['auth_token'])
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
def test_registered_with_already_registered_user(self):
|
||||||
|
""" Test registration with already registered email"""
|
||||||
|
user = User(email='joe@gmail.com', password='test')
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
with self.client:
|
||||||
|
response = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'fail')
|
||||||
|
self.assertTrue(
|
||||||
|
data['message'] == 'User already exists. Please Log in.')
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 202)
|
||||||
|
|
||||||
|
def test_registered_user_login(self):
|
||||||
|
""" Test for login of registered-user login """
|
||||||
|
with self.client:
|
||||||
|
# user registration
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
data_register = json.loads(resp_register.data.decode())
|
||||||
|
self.assertTrue(data_register['status'] == 'success')
|
||||||
|
self.assertTrue(
|
||||||
|
data_register['message'] == 'Successfully registered.'
|
||||||
|
)
|
||||||
|
self.assertTrue(data_register['auth_token'])
|
||||||
|
self.assertTrue(resp_register.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_register.status_code, 201)
|
||||||
|
# registered user login
|
||||||
|
response = self.client.post(
|
||||||
|
'/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
nickname='test_nick',
|
||||||
|
email='joe@gmail.com',
|
||||||
|
password='123456'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['message'] == 'Successfully logged in.')
|
||||||
|
self.assertTrue(data['auth_token'])
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_non_registered_user_login(self):
|
||||||
|
""" Test for login of non-registered user """
|
||||||
|
with self.client:
|
||||||
|
response = self.client.post(
|
||||||
|
'/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
nickname='test_nick',
|
||||||
|
email='joe@gmail.com',
|
||||||
|
password='123456'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'fail')
|
||||||
|
self.assertTrue(data['message'] == 'User does not exist.')
|
||||||
|
self.assertTrue(response.content_type == 'application/json')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_user_status(self):
|
||||||
|
""" Test for user status """
|
||||||
|
with self.client:
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
response = self.client.get(
|
||||||
|
'/auth/status',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_register.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['data'] is not None)
|
||||||
|
self.assertTrue(data['data']['email'] == 'joe@gmail.com')
|
||||||
|
# self.assertTrue(data['data']['admin'] is 'true' or 'false')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_valid_logout(self):
|
||||||
|
""" Test for logout before token expires """
|
||||||
|
with self.client:
|
||||||
|
# user registration
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
data_register = json.loads(resp_register.data.decode())
|
||||||
|
self.assertTrue(data_register['status'] == 'success')
|
||||||
|
self.assertTrue(
|
||||||
|
data_register['message'] == 'Successfully registered.')
|
||||||
|
self.assertTrue(data_register['auth_token'])
|
||||||
|
self.assertTrue(resp_register.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_register.status_code, 201)
|
||||||
|
# user login
|
||||||
|
resp_login = self.client.post(
|
||||||
|
'/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='joe@gmail.com',
|
||||||
|
password='123456'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data_login = json.loads(resp_login.data.decode())
|
||||||
|
self.assertTrue(data_login['status'] == 'success')
|
||||||
|
self.assertTrue(data_login['message'] == 'Successfully logged in.')
|
||||||
|
self.assertTrue(data_login['auth_token'])
|
||||||
|
self.assertTrue(resp_login.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_login.status_code, 200)
|
||||||
|
# valid token logout
|
||||||
|
response = self.client.post(
|
||||||
|
'/auth/logout',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'success')
|
||||||
|
self.assertTrue(data['message'] == 'Successfully logged out.')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_invalid_logout(self):
|
||||||
|
""" Testing logout after the token expires """
|
||||||
|
with self.client:
|
||||||
|
# user registration
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
data_register = json.loads(resp_register.data.decode())
|
||||||
|
self.assertTrue(data_register['status'] == 'success')
|
||||||
|
self.assertTrue(
|
||||||
|
data_register['message'] == 'Successfully registered.')
|
||||||
|
self.assertTrue(data_register['auth_token'])
|
||||||
|
self.assertTrue(resp_register.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_register.status_code, 201)
|
||||||
|
# user login
|
||||||
|
resp_login = self.client.post(
|
||||||
|
'/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='joe@gmail.com',
|
||||||
|
password='123456'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data_login = json.loads(resp_login.data.decode())
|
||||||
|
self.assertTrue(data_login['status'] == 'success')
|
||||||
|
self.assertTrue(data_login['message'] == 'Successfully logged in.')
|
||||||
|
self.assertTrue(data_login['auth_token'])
|
||||||
|
self.assertTrue(resp_login.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_login.status_code, 200)
|
||||||
|
# invalid token logout
|
||||||
|
time.sleep(6)
|
||||||
|
response = self.client.post(
|
||||||
|
'/auth/logout',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(response.data)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'fail')
|
||||||
|
self.assertTrue(
|
||||||
|
data['message'] == 'Signature expired. Please log in again.')
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
def test_valid_blacklisted_token_logout(self):
|
||||||
|
""" Test for logout after a valid token gets blacklisted """
|
||||||
|
with self.client:
|
||||||
|
# user registration
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
data_register = json.loads(resp_register.data.decode())
|
||||||
|
self.assertTrue(data_register['status'] == 'success')
|
||||||
|
self.assertTrue(
|
||||||
|
data_register['message'] == 'Successfully registered.')
|
||||||
|
self.assertTrue(data_register['auth_token'])
|
||||||
|
self.assertTrue(resp_register.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_register.status_code, 201)
|
||||||
|
# user login
|
||||||
|
resp_login = self.client.post(
|
||||||
|
'/auth/login',
|
||||||
|
data=json.dumps(dict(
|
||||||
|
email='joe@gmail.com',
|
||||||
|
password='123456'
|
||||||
|
)),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
data_login = json.loads(resp_login.data.decode())
|
||||||
|
self.assertTrue(data_login['status'] == 'success')
|
||||||
|
self.assertTrue(data_login['message'] == 'Successfully logged in.')
|
||||||
|
self.assertTrue(data_login['auth_token'])
|
||||||
|
self.assertTrue(resp_login.content_type == 'application/json')
|
||||||
|
self.assertEqual(resp_login.status_code, 200)
|
||||||
|
# blacklist a valid token
|
||||||
|
blacklist_token = BlacklistToken(
|
||||||
|
token=json.loads(resp_login.data.decode())['auth_token'])
|
||||||
|
db.session.add(blacklist_token)
|
||||||
|
db.session.commit()
|
||||||
|
# blacklisted valid token logout
|
||||||
|
response = self.client.post(
|
||||||
|
'/auth/logout',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_login.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'fail')
|
||||||
|
self.assertTrue(data['message'] == 'Token blacklisted. Please log in again.')
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
def test_valid_blacklisted_token_user(self):
|
||||||
|
""" Test for user status with a blacklisted valid token """
|
||||||
|
with self.client:
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
# blacklist a valid token
|
||||||
|
blacklist_token = BlacklistToken(
|
||||||
|
token=json.loads(resp_register.data.decode())['auth_token'])
|
||||||
|
db.session.add(blacklist_token)
|
||||||
|
db.session.commit()
|
||||||
|
response = self.client.get(
|
||||||
|
'/auth/status',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer ' + json.loads(
|
||||||
|
resp_register.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'fail')
|
||||||
|
self.assertTrue(data['message'] == 'Token blacklisted. Please log in again.')
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
def test_user_status_malformed_bearer_token(self):
|
||||||
|
""" Test for user status with malformed bearer token"""
|
||||||
|
with self.client:
|
||||||
|
resp_register = register_user(self, 'joe@gmail.com', '123456')
|
||||||
|
response = self.client.get(
|
||||||
|
'/auth/status',
|
||||||
|
headers=dict(
|
||||||
|
Authorization='Bearer' + json.loads(
|
||||||
|
resp_register.data.decode()
|
||||||
|
)['auth_token']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
self.assertTrue(data['status'] == 'fail')
|
||||||
|
self.assertTrue(data['message'] == 'Bearer token malformed.')
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
36
backend/tests/test_user_model.py
Normal file
36
backend/tests/test_user_model.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
from backend.models.user_model import User
|
||||||
|
from backend.tests.base import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserModel(BaseTestCase):
|
||||||
|
|
||||||
|
def test_encode_auth_token(self):
|
||||||
|
user = User(
|
||||||
|
nickname='testheini',
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
auth_token = user.encode_auth_token()
|
||||||
|
self.assertTrue(isinstance(auth_token, bytes))
|
||||||
|
|
||||||
|
def test_decode_auth_token(self):
|
||||||
|
user = User(
|
||||||
|
nickname='testheini',
|
||||||
|
email='test@test.com',
|
||||||
|
password='test'
|
||||||
|
)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
auth_token = user.encode_auth_token()
|
||||||
|
self.assertTrue(isinstance(auth_token, bytes))
|
||||||
|
self.assertTrue(User.decode_auth_token(
|
||||||
|
auth_token.decode("utf-8")) == 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
2
backend/tools/__init__.py
Normal file
2
backend/tools/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze, KIT
|
||||||
|
|
||||||
53
backend/tools/campus_rooms.py
Normal file
53
backend/tools/campus_rooms.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from campus_api_client.campus_management_api_client import CampusManagementApiClient
|
||||||
|
|
||||||
|
from backend import Config
|
||||||
|
|
||||||
|
|
||||||
|
def get_campus_rooms():
|
||||||
|
cac = CampusManagementApiClient(user=Config.CAMPUS_MANAGEMENT_USER, pw=Config.CAMPUS_MANAGEMENT_PW)
|
||||||
|
|
||||||
|
parsed_rooms = []
|
||||||
|
count = 0
|
||||||
|
re_string = r"^(\d\d.\d\d)?\s(.*)"
|
||||||
|
re_exp = re.compile(re_string)
|
||||||
|
for r in cac.get_rooms():
|
||||||
|
name = r.get('name')
|
||||||
|
# print(f"## {r.get('name')}: {r.get('roomNo')}")
|
||||||
|
building_number = r.get('buildingNo', None)
|
||||||
|
building_name = r.get('building')
|
||||||
|
if building_number is None or building_number == '':
|
||||||
|
count += 1
|
||||||
|
# print(r)
|
||||||
|
match = re_exp.match(building_name)
|
||||||
|
if match is not None:
|
||||||
|
building_number, building_name = match.groups()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
building_name = building_name.split(building_number)[1].strip()
|
||||||
|
building_name = None if building_name == "" else building_name
|
||||||
|
except IndexError: # ignore index error
|
||||||
|
pass
|
||||||
|
# print(f"Building No {r.get('buildingNo')}: {r.get('building')}")
|
||||||
|
# print(building_name)
|
||||||
|
# print(building_number)
|
||||||
|
|
||||||
|
if building_number is not None and building_number != "" and name.startswith(building_number):
|
||||||
|
name = name.split(building_number, 1)[1].strip()
|
||||||
|
elif building_number is not None and name.startswith(f"Geb. {building_number}"):
|
||||||
|
name = name.split(f"Geb. {building_number}", 1)[1].strip()
|
||||||
|
room = {'name': name,
|
||||||
|
'alternate_name': r.get('internalName'),
|
||||||
|
'external_id': r.get('guid'),
|
||||||
|
'room_number': r.get('roomNo'),
|
||||||
|
'building_name': building_name,
|
||||||
|
'building_number': building_number,
|
||||||
|
'coordinates': r.get('coordinates')}
|
||||||
|
parsed_rooms.append(room)
|
||||||
|
return parsed_rooms
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
rooms = get_campus_rooms()
|
||||||
|
print(len(rooms))
|
||||||
1
backend/tools/current_rooms_cache.json
Normal file
1
backend/tools/current_rooms_cache.json
Normal file
File diff suppressed because one or more lines are too long
19
backend/tools/exception_decorator.py
Normal file
19
backend/tools/exception_decorator.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from backend import LrcException
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.tools.exception_decorator")
|
||||||
|
|
||||||
|
|
||||||
|
def exception_decorator(*exceptions):
|
||||||
|
def decorator(func):
|
||||||
|
def new_func(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
ret = func(*args, **kwargs)
|
||||||
|
return ret
|
||||||
|
except exceptions as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
raise LrcException(e)
|
||||||
|
|
||||||
|
return new_func
|
||||||
|
|
||||||
|
return decorator
|
||||||
17
backend/tools/helpers.py
Normal file
17
backend/tools/helpers.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_md5_checksum(string_to_md5_sum: str):
|
||||||
|
return hashlib.md5(string_to_md5_sum.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def file_md5(fname: str) -> str:
|
||||||
|
hash_md5 = hashlib.md5()
|
||||||
|
with open(fname, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
|
hash_md5.update(chunk)
|
||||||
|
return hash_md5.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(file_md5(__file__))
|
||||||
56
backend/tools/import_excel_recorder_list.py
Normal file
56
backend/tools/import_excel_recorder_list.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import json
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
import xlrd
|
||||||
|
|
||||||
|
MAX_CONSECUTIVE_EMPTY_ROWS_BEFORE_CANCEL = 5
|
||||||
|
file_location = "/home/tobias/tmp/Hoersaal-Rec-Liste.xlsx"
|
||||||
|
|
||||||
|
wb = xlrd.open_workbook(file_location)
|
||||||
|
sheet = wb.sheet_by_index(0)
|
||||||
|
|
||||||
|
headers = None
|
||||||
|
recorders = []
|
||||||
|
a_lot_of_empty_rows = False
|
||||||
|
consecutive_empty_rows = 0
|
||||||
|
ix = 0
|
||||||
|
while not a_lot_of_empty_rows:
|
||||||
|
try:
|
||||||
|
vals = sheet.row_values(ix)
|
||||||
|
except IndexError:
|
||||||
|
break
|
||||||
|
if len(set(vals)) == 1 and set(vals).pop() == '':
|
||||||
|
consecutive_empty_rows += 1
|
||||||
|
if consecutive_empty_rows > MAX_CONSECUTIVE_EMPTY_ROWS_BEFORE_CANCEL:
|
||||||
|
a_lot_of_empty_rows = True
|
||||||
|
else:
|
||||||
|
consecutive_empty_rows = 0
|
||||||
|
if len(set(vals)) > 5 and headers is None: # get table header
|
||||||
|
headers = {ix: vals[ix] for ix in range(0, len(vals))}
|
||||||
|
elif len(set(vals)) > 5: # regular rows
|
||||||
|
recorders.append({headers[ix]: vals[ix] for ix in range(0, len(vals))})
|
||||||
|
ix += 1
|
||||||
|
if ix >= 100:
|
||||||
|
a_lot_of_empty_rows = True
|
||||||
|
|
||||||
|
#pprint(recorders)
|
||||||
|
print(len(recorders))
|
||||||
|
|
||||||
|
nicely_formatted_recorders = []
|
||||||
|
for r in recorders:
|
||||||
|
rec = {'name': r['Opencast / CM'],
|
||||||
|
'building': r['Gebäude'],
|
||||||
|
'room': r['Hörsaal'],
|
||||||
|
'username': r['Benutzer'],
|
||||||
|
'password': r['PW'],
|
||||||
|
'ip': r['IP'],
|
||||||
|
'mac': r['MAC'],
|
||||||
|
'type': 'SMP 351' if 'SMP 351' in r['Rekorder-Typ'] else r['Rekorder-Typ'],
|
||||||
|
'additional_camera': r['Zus. Kamera'] == 'Ja',
|
||||||
|
'firmware_version': r['FW – Vers.'],
|
||||||
|
'network_name': r['DNS'],
|
||||||
|
'description': json.dumps(
|
||||||
|
{'comment': r['Bemerkung'], 'network_port': r['Dosennummer'], 'rsync_name': r['Rsync-Name']})}
|
||||||
|
nicely_formatted_recorders.append(rec)
|
||||||
|
|
||||||
|
print(json.dumps(nicely_formatted_recorders))
|
||||||
214
backend/tools/model_updater.py
Normal file
214
backend/tools/model_updater.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from json import dumps
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from sqlalchemy import and_
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from backend import db
|
||||||
|
from backend.models import Room
|
||||||
|
from backend.models.recorder_model import RecorderModel, RecorderCommand, Recorder
|
||||||
|
from backend.recorder_adapters import get_defined_recorder_adapters
|
||||||
|
from backend.tools.helpers import calculate_md5_checksum
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc." + __name__)
|
||||||
|
|
||||||
|
KNOWN_RECORDERS = {re.compile(r'(?P<name>SMP)[\s]*(?P<number>[\d]+)[\s]*.?[\s]*(?P<options>[\S]*)'): 'SMP',
|
||||||
|
re.compile(
|
||||||
|
r'(?P<name>LectureRecorder X2|LectureRecorder|VGADVI Recorder|DVI Broadcaster DL|DVIRecorderDL)'): 'Epiphan'}
|
||||||
|
|
||||||
|
|
||||||
|
def create_recorder_commands_for_recorder_adapter(command_definitions: dict, recorder_model: RecorderModel):
|
||||||
|
logger.debug("Received {} command definitions to create "
|
||||||
|
"for recoder model {}".format(len(command_definitions),
|
||||||
|
recorder_model.record_adapter_id))
|
||||||
|
existing_recorder_commands = RecorderCommand.query.filter(
|
||||||
|
and_(
|
||||||
|
RecorderCommand.name.in_([recorder_model.record_adapter_id + ":" + k for k in command_definitions.keys()])),
|
||||||
|
RecorderCommand.recorder_model == recorder_model)
|
||||||
|
existing_commands = set()
|
||||||
|
for existing_command in existing_recorder_commands:
|
||||||
|
existing_commands.add(existing_command.name)
|
||||||
|
args = command_definitions.get(existing_command.name)
|
||||||
|
if dumps(existing_command.parameters) != dumps(args):
|
||||||
|
logger.warning(
|
||||||
|
"The function definition {} collides with an existing definition of the same name "
|
||||||
|
"but different parameters!".format(
|
||||||
|
existing_command.name))
|
||||||
|
existing_command.last_time_modified = datetime.utcnow()
|
||||||
|
existing_command.parameters = args
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
for c_d in set(command_definitions.keys()) - existing_commands:
|
||||||
|
args = command_definitions.get(c_d)
|
||||||
|
# print(args)
|
||||||
|
# create new recorder command(s)
|
||||||
|
r_c = RecorderCommand(name=recorder_model.record_adapter_id + ":" + c_d, parameters=args,
|
||||||
|
recorder_model=recorder_model)
|
||||||
|
db.session.add(r_c)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def update_recorder_models_database(drop: bool = False):
|
||||||
|
if drop:
|
||||||
|
for r in RecorderModel.get_all():
|
||||||
|
db.session.delete(r)
|
||||||
|
db.session.commit()
|
||||||
|
r_as = get_defined_recorder_adapters()
|
||||||
|
for r_a in r_as:
|
||||||
|
if r_a.get('class') is None: # skip modules without class (helpers.py, e.g.)
|
||||||
|
continue
|
||||||
|
logger.info("Checking Recorder Adapter {} for possible updates".format(r_a["id"]))
|
||||||
|
try:
|
||||||
|
r_m = RecorderModel.get_by_adapter_id(r_a["id"])
|
||||||
|
model_checksum = calculate_md5_checksum(dumps(r_a["commands"]))
|
||||||
|
logger.debug("Recorder Adapter commands checksum: {}".format(model_checksum))
|
||||||
|
logger.debug(r_m)
|
||||||
|
if r_m is None:
|
||||||
|
r_m = RecorderModel(record_adapter_id=r_a["id"], model_name=r_a["name"], checksum=model_checksum,
|
||||||
|
**r_a.get('class').get_recorder_params())
|
||||||
|
db.session.add(r_m)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.refresh(r_m)
|
||||||
|
logger.debug("Creating command definitions for rec mod adapter {}".format(r_m.record_adapter_id))
|
||||||
|
create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m)
|
||||||
|
else:
|
||||||
|
logger.debug("Model command checksum already in db: {}".format(r_m.checksum))
|
||||||
|
if r_m.model_name != r_a["name"]:
|
||||||
|
r_m.model_name = r_a["name"]
|
||||||
|
r_m.last_time_modified = datetime.utcnow()
|
||||||
|
if model_checksum != r_m.checksum:
|
||||||
|
create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m)
|
||||||
|
r_m.last_time_modified = datetime.utcnow()
|
||||||
|
r_m.checksum = model_checksum
|
||||||
|
r_m.last_checksum_change = datetime.utcnow()
|
||||||
|
logger.debug("Updating command definitions for rec mod adapter {}".format(r_m.record_adapter_id))
|
||||||
|
except IntegrityError as e:
|
||||||
|
logger.error(e)
|
||||||
|
db.session.rollback()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_recorder_room(rec: dict):
|
||||||
|
rooms = Room.get_by_building_number(rec.get('building', None))
|
||||||
|
if rooms.count() <= 0:
|
||||||
|
logger.warning("Building {} unknown! Can not find room for recorder {}.".format(rec['building'], rec['name']))
|
||||||
|
return None
|
||||||
|
if rooms.count() == 1:
|
||||||
|
return rooms.first()
|
||||||
|
room_name = rec.get('room')
|
||||||
|
room_name = room_name.replace('–', ' ')
|
||||||
|
for room in rooms:
|
||||||
|
if all([r_n in room.name for r_n in room_name.split()]):
|
||||||
|
return room
|
||||||
|
logger.warning("No room found for recorder {}".format(rec['name']))
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_recorders():
|
||||||
|
f = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'models', 'initial_recorders.json'))
|
||||||
|
with open(f, 'r') as json_file:
|
||||||
|
recorders = json.load(json_file)['recorders']
|
||||||
|
for r in recorders:
|
||||||
|
type = r.get('type')
|
||||||
|
firmware_version = r.get('firmware_version', None)
|
||||||
|
room_rec_name = r.get('name')
|
||||||
|
username = r.get('username', None)
|
||||||
|
password = r.get('password', None)
|
||||||
|
mac = r.get('mac', None)
|
||||||
|
ip4 = r.get('ip', None)
|
||||||
|
additional_camera = r.get('additional_camera', False)
|
||||||
|
description = r.get('description', '')
|
||||||
|
for k_r in KNOWN_RECORDERS:
|
||||||
|
if match := k_r.search(type):
|
||||||
|
name = match.groupdict().get('name')
|
||||||
|
model_number = match.groupdict().get('number', None)
|
||||||
|
|
||||||
|
options = match.groupdict().get('options', None)
|
||||||
|
if options is not None and options != "":
|
||||||
|
options = options.split(',')
|
||||||
|
|
||||||
|
if model_number is not None:
|
||||||
|
model_name = name + " " + model_number
|
||||||
|
model_search_name = name + model_number[:-1] # just get prefix (remove last digit); SMP 35x -> 35
|
||||||
|
else:
|
||||||
|
model_name = name
|
||||||
|
model_search_name = KNOWN_RECORDERS[k_r]
|
||||||
|
rec_model = RecorderModel.get_where_adapter_id_contains(model_search_name)
|
||||||
|
rec = Recorder.get_by_mac(mac)
|
||||||
|
if rec is None:
|
||||||
|
rec = Recorder.get_by_name(room_rec_name + " Recorder")
|
||||||
|
if rec is None:
|
||||||
|
rec = Recorder()
|
||||||
|
rec.mac = mac
|
||||||
|
db.session.add(rec)
|
||||||
|
rec.name = room_rec_name + " Recorder"
|
||||||
|
rec.model_name = model_name
|
||||||
|
rec.recorder_model = rec_model
|
||||||
|
rec.username = username
|
||||||
|
rec.password = password
|
||||||
|
rec.firmware_version = firmware_version
|
||||||
|
rec.ip = ip4
|
||||||
|
rec.additional_camera_connected = additional_camera
|
||||||
|
rec.additional_note = description
|
||||||
|
rec.configured_options = options
|
||||||
|
|
||||||
|
rec.room = get_recorder_room(r)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.refresh(rec)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def add_test_recorder():
|
||||||
|
test_rec_mac = "00:05:A6:16:A3:E4"
|
||||||
|
rec = Recorder.get_by_mac(test_rec_mac)
|
||||||
|
if rec is None:
|
||||||
|
rec = Recorder()
|
||||||
|
rec.mac = test_rec_mac
|
||||||
|
rec.name = "Test SMP (MZ)"
|
||||||
|
rec.model_name = "SMP 352"
|
||||||
|
rec.recorder_model = RecorderModel.get_where_adapter_id_contains("SMP 35")
|
||||||
|
rec.username = "admin"
|
||||||
|
rec.password = "123mzsmp"
|
||||||
|
rec.firmware_version = "2.11b0"
|
||||||
|
rec.ip = "172.22.246.207"
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for r in Room.get_all():
|
||||||
|
# print(r)
|
||||||
|
# exit()
|
||||||
|
|
||||||
|
commands = RecorderCommand.get_all()
|
||||||
|
for c in commands:
|
||||||
|
print(c)
|
||||||
|
print(c.recorder_model)
|
||||||
|
|
||||||
|
for r_m in RecorderModel.get_all():
|
||||||
|
print(r_m.recorder_commands)
|
||||||
|
|
||||||
|
exit()
|
||||||
|
recorders = Recorder.get_all()
|
||||||
|
for r in recorders:
|
||||||
|
if r.room is None:
|
||||||
|
print("{}: {}".format(r, r.room))
|
||||||
|
# db.drop_all()
|
||||||
|
# db.create_all()
|
||||||
|
update_recorder_models_database()
|
||||||
|
create_default_recorders()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# print(get_recorder_room({"room": "Grosser Hörsaal Bauingenieure", "building": "10.50"}))
|
||||||
|
# print(get_recorder_room({"room": "Grosser Hörsaal Bauingenieure", "building": "30.95"}))
|
||||||
319
backend/tools/recorder_state_checker.py
Normal file
319
backend/tools/recorder_state_checker.py
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from io import StringIO
|
||||||
|
from logging.handlers import MemoryHandler
|
||||||
|
from pprint import pprint
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
from multiprocessing.context import TimeoutError
|
||||||
|
|
||||||
|
from ics import Calendar
|
||||||
|
|
||||||
|
from backend import LrcException
|
||||||
|
from backend.config import Config
|
||||||
|
from backend.models import Recorder, RecorderModel
|
||||||
|
from backend.recorder_adapters import RecorderAdapter
|
||||||
|
|
||||||
|
from backend.recorder_adapters.epiphan_base import Epiphan
|
||||||
|
from backend.recorder_adapters.extron_smp import SMP35x
|
||||||
|
from backend.tools.recorder_streams_sanity_checks import check_frame_is_valid, check_if_audio_is_valid
|
||||||
|
from backend.tools.send_mail import send_error_mail, get_smtp_error_handler
|
||||||
|
|
||||||
|
from backend.tools.exception_decorator import exception_decorator
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.tools.simple_state_checker")
|
||||||
|
|
||||||
|
smtp_error_handler = get_smtp_error_handler(subject="Errors have been detected while checking recorder states!")
|
||||||
|
# mem_handler = MemoryHandler(capacity=100, flushLevel=logging.FATAL, target=smtp_error_handler)
|
||||||
|
# mem_handler.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
rec_err_state_log_stream = StringIO()
|
||||||
|
rec_err_state_log_stream_handler = logging.StreamHandler(stream=rec_err_state_log_stream)
|
||||||
|
rec_err_state_log_stream_handler.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
logger.addHandler(rec_err_state_log_stream_handler)
|
||||||
|
# logger.addHandler(mem_handler)
|
||||||
|
|
||||||
|
session = requests.session()
|
||||||
|
session.auth = HTTPBasicAuth(Config.OPENCAST_USER, Config.OPENCAST_PW)
|
||||||
|
|
||||||
|
config = {'service_urls': {}}
|
||||||
|
|
||||||
|
recorders = None
|
||||||
|
|
||||||
|
agent_states_lock = threading.RLock()
|
||||||
|
agent_states = {}
|
||||||
|
|
||||||
|
|
||||||
|
@exception_decorator(ConnectionError)
|
||||||
|
def get_service_url(service_type: str):
|
||||||
|
if service_type in config['service_urls']:
|
||||||
|
return config['service_urls'][service_type]
|
||||||
|
params = {'serviceType': service_type}
|
||||||
|
url = Config.OPENCAST_URL + "/services/available.json"
|
||||||
|
res = session.get(url, params=params)
|
||||||
|
if res.ok:
|
||||||
|
service = res.json()["services"]["service"]
|
||||||
|
config["service_urls"][service_type] = service["host"] + \
|
||||||
|
service["path"]
|
||||||
|
return service["host"] + service["path"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@exception_decorator(ConnectionError)
|
||||||
|
def get_calender(rec_id):
|
||||||
|
params = {'agentid': rec_id}
|
||||||
|
url = get_service_url('org.opencastproject.scheduler') + "/calendars"
|
||||||
|
res = session.get(url, params=params)
|
||||||
|
if res.ok:
|
||||||
|
return Calendar(res.text)
|
||||||
|
raise LrcException(res.text, res.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@exception_decorator(ConnectionError)
|
||||||
|
def get_capture_agents():
|
||||||
|
url = get_service_url("org.opencastproject.capture.admin") + "/agents.json"
|
||||||
|
res = session.get(url)
|
||||||
|
if res.ok:
|
||||||
|
return res.json()["agents"]["agent"]
|
||||||
|
raise LrcException(res.text, res.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def get_recorder_details_old():
|
||||||
|
"""Temporary implementation using initial_recorders.json. Should be replaced by DB layer later!"""
|
||||||
|
global recorders
|
||||||
|
if recorders is None:
|
||||||
|
f = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'models', 'initial_recorders.json'))
|
||||||
|
with open(f, 'r') as json_file:
|
||||||
|
recorders = json.load(json_file)['recorders']
|
||||||
|
return recorders
|
||||||
|
|
||||||
|
|
||||||
|
def get_recorder_details():
|
||||||
|
"""New implementation using DB"""
|
||||||
|
global recorders
|
||||||
|
if recorders is None:
|
||||||
|
recorders = list(Recorder.get_all())
|
||||||
|
return recorders
|
||||||
|
|
||||||
|
|
||||||
|
def get_recorder_by_name(name: str):
|
||||||
|
for r in get_recorder_details():
|
||||||
|
logger.debug("Got recorder {}".format(r.get("name")))
|
||||||
|
if r.get("name") == name or r.get("name") + " Recorder" == name or r.get("name") == name + " Recorder":
|
||||||
|
return r
|
||||||
|
logger.error("Could not find recorder for name {}".format(name))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def notify_users_of_problem(msg: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_recorder_adapter(recorder_info: Union[dict, Recorder]) -> RecorderAdapter:
|
||||||
|
if recorder_info is None:
|
||||||
|
raise LrcException("Could not find recorder Adapter as recorder info was NONE!")
|
||||||
|
try:
|
||||||
|
type = recorder_info.get("type")
|
||||||
|
except KeyError:
|
||||||
|
type = RecorderModel.get_by_id(recorder_info.get('recorder_model_id')).model_name
|
||||||
|
try:
|
||||||
|
if "SMP" in type:
|
||||||
|
rec = SMP35x(recorder_info.get('ip'), recorder_info.get('password'))
|
||||||
|
else:
|
||||||
|
rec = Epiphan(recorder_info.get('ip'), recorder_info.get("username"), recorder_info.get("password"))
|
||||||
|
return rec
|
||||||
|
except LrcException:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def check_stream_sanity(recorder_agent: Union[Recorder, dict], recorder_adapter: RecorderAdapter = None):
|
||||||
|
try:
|
||||||
|
if recorder_adapter is None:
|
||||||
|
recorder_info = get_recorder_by_name(recorder_agent.get('name'))
|
||||||
|
recorder_adapter = get_recorder_adapter(recorder_info)
|
||||||
|
if not recorder_adapter.is_recording():
|
||||||
|
return True, "not recording, so there is no stream!", recorder_agent.get('name')
|
||||||
|
except LrcException:
|
||||||
|
return False, "Could not determine if recorder is recording!", recorder_agent.get('name')
|
||||||
|
if recorder_agent.get('archive_stream1') is None and recorder_agent.get(
|
||||||
|
'archive_stream2') is None: # fall back to default names and rtsp
|
||||||
|
archive_stream_1_url = "rtsp://{}/{}".format(recorder_adapter.address, Config.DEFAULT_ARCHIVE_STREAM_1_NAME)
|
||||||
|
archive_stream_2_url = "rtsp://{}/{}".format(recorder_adapter.address, Config.DEFAULT_ARCHIVE_STREAM_2_NAME)
|
||||||
|
else:
|
||||||
|
archive_stream_1_url = recorder_agent.get('archive_stream1')
|
||||||
|
archive_stream_2_url = recorder_agent.get('archive_stream2')
|
||||||
|
|
||||||
|
frame_msg = frame_msg2 = sound_msg = sound_msg2 = "unknown"
|
||||||
|
for i in range(0, Config.STREAM_SANITY_CHECK_RETRIES):
|
||||||
|
frame_ok, frame_msg = check_frame_is_valid(archive_stream_1_url, raise_errors=False)
|
||||||
|
if not frame_ok:
|
||||||
|
logger.warning(
|
||||||
|
"Archive stream 1 ({}) of {} is not ok (frame): {}".format(archive_stream_1_url,
|
||||||
|
recorder_agent.get('name'),
|
||||||
|
frame_msg))
|
||||||
|
frame_ok, frame_msg2 = check_frame_is_valid(archive_stream_2_url, raise_errors=False)
|
||||||
|
if not frame_ok:
|
||||||
|
logger.warning(
|
||||||
|
"Archive stream 2 {} of ({}) is not ok: {} (frame)".format(archive_stream_2_url,
|
||||||
|
recorder_agent.get('name'),
|
||||||
|
frame_msg2))
|
||||||
|
|
||||||
|
sound_ok, sound_msg = check_if_audio_is_valid(archive_stream_1_url, raise_errors=False)
|
||||||
|
if not sound_ok:
|
||||||
|
logger.warning(
|
||||||
|
"Archive stream 1 {} of ({}) is not ok (audio): {}".format(archive_stream_1_url,
|
||||||
|
recorder_agent.get('name'),
|
||||||
|
sound_msg))
|
||||||
|
sound_ok, sound_msg2 = check_if_audio_is_valid(archive_stream_2_url, raise_errors=False)
|
||||||
|
if not sound_ok:
|
||||||
|
logger.warning(
|
||||||
|
"Archive stream 2 {} of ({}) is not ok (audio): {}".format(archive_stream_2_url,
|
||||||
|
recorder_agent.get('name'),
|
||||||
|
sound_msg2))
|
||||||
|
|
||||||
|
if frame_ok and sound_ok:
|
||||||
|
return True, "At least one archive stream is fine (audio and image)! " \
|
||||||
|
"(s1: a: {}, v: {}, s2: a: {}, v: {})".format(sound_msg, frame_msg, sound_msg2,
|
||||||
|
frame_msg2), recorder_agent.get('name')
|
||||||
|
|
||||||
|
else:
|
||||||
|
time.sleep(Config.STREAM_SANITY_CHECK_INTERVAL_SEC)
|
||||||
|
error_msg = "After {} retries, stream checks failed and returned: archive_stream1: audio:{}, frame:{}, " \
|
||||||
|
"archive_stream2: audio:{}, frame:{}".format(Config.STREAM_SANITY_CHECK_RETRIES, sound_msg,
|
||||||
|
frame_msg, sound_msg2, frame_msg2)
|
||||||
|
logger.error(error_msg)
|
||||||
|
return False, error_msg, recorder_agent.get('name') # stream sanity check failed!
|
||||||
|
|
||||||
|
|
||||||
|
def check_capture_agent_state(recorder_agent: Union[Recorder, dict]):
|
||||||
|
if recorder_agent.get('offline', False):
|
||||||
|
logger.info("OK - Recorder {} is in offline / maintenance mode".format(recorder_agent.get('name')))
|
||||||
|
return True, "Recorder is in offline / maintenance mode", recorder_agent.get('name')
|
||||||
|
agent_state_error_msg = None
|
||||||
|
logger.debug("Checking Agent {}".format(recorder_agent.get('name')))
|
||||||
|
try:
|
||||||
|
c = get_calender(recorder_agent.get('name'))
|
||||||
|
except LrcException:
|
||||||
|
error_msg = "Could not get calender of recorder agent: {}!".format(recorder_agent.get('name'))
|
||||||
|
logger.fatal(error_msg)
|
||||||
|
return False, error_msg, recorder_agent.get('name')
|
||||||
|
is_recording_in_calendar = len(list(c.timeline.now())) >= 1
|
||||||
|
if is_recording_in_calendar:
|
||||||
|
logger.info("{} has entry in Calender and should therefore be recording... checking now!".format(
|
||||||
|
recorder_agent.get('name')))
|
||||||
|
if recorder_agent['state'] == "capturing":
|
||||||
|
recorder_info = get_recorder_by_name(recorder_agent.get('name'))
|
||||||
|
try:
|
||||||
|
rec = get_recorder_adapter(recorder_info)
|
||||||
|
if rec.is_recording():
|
||||||
|
logger.info("OK – recorder {} is recording :)".format(recorder_agent.get('name')))
|
||||||
|
with agent_states_lock:
|
||||||
|
agent_states[recorder_agent.get('name')] = 'OK - recorder is recording'
|
||||||
|
else:
|
||||||
|
logger.info(rec.get_recording_status())
|
||||||
|
logger.error(
|
||||||
|
"FATAL - recorder {} must be recording but is not!!!!".format(recorder_agent.get('name')))
|
||||||
|
agent_state_error_msg = "FATAL - recorder must be recording but is not!"
|
||||||
|
with agent_states_lock:
|
||||||
|
agent_states[recorder_agent['name']] = 'FATAL - recorder is NOT recording, but should!'
|
||||||
|
except LrcException as e:
|
||||||
|
logger.fatal("Exception occurred: {}".format(str(e)))
|
||||||
|
logger.error("Could not check state of recorder {}, Address: {}".format(recorder_agent.get('name'),
|
||||||
|
recorder_info.get('ip')))
|
||||||
|
else:
|
||||||
|
logger.error("FATAL: {} is not in capturing state...but should be!!".format(recorder_agent.get('name')))
|
||||||
|
agent_state_error_msg = "FATAL - is not in capturing state...but should be!"
|
||||||
|
else:
|
||||||
|
recorder_info = get_recorder_by_name(recorder_agent.get('name'))
|
||||||
|
try:
|
||||||
|
rec = get_recorder_adapter(recorder_info)
|
||||||
|
if rec.is_recording():
|
||||||
|
logger.error("FATAL - recorder must not be recording!!!!")
|
||||||
|
agent_state_error_msg = "FATAL - is not in capturing state...but should be!"
|
||||||
|
with agent_states_lock:
|
||||||
|
agent_states[recorder_agent.get('name')] = 'FATAL - recorder IS recording, but should NOT!'
|
||||||
|
else:
|
||||||
|
logger.info("OK – recorder is not recording :)")
|
||||||
|
with agent_states_lock:
|
||||||
|
agent_states[recorder_agent.get('name')] = 'OK - recorder is NOT recording'
|
||||||
|
except LrcException as e:
|
||||||
|
logger.fatal("Exception occurred: {}".format(str(e)))
|
||||||
|
logger.error("Could not check state of recorder {}, Address: {}".format(recorder_agent.get('name'),
|
||||||
|
recorder_info.get('ip')))
|
||||||
|
agent_state_error_msg = "FATAL - Could not check state of recorder! Address: {}".format(
|
||||||
|
recorder_info.get('ip'))
|
||||||
|
|
||||||
|
if agent_state_error_msg is None:
|
||||||
|
return True, agent_states[recorder_agent.get('name')], recorder_agent.get('name')
|
||||||
|
return False, agent_state_error_msg, recorder_agent.get('name')
|
||||||
|
|
||||||
|
|
||||||
|
def ping_capture_agent(recorder_agent: Union[Recorder, dict]):
|
||||||
|
if recorder_agent.get('offline', False):
|
||||||
|
print("is offline!")
|
||||||
|
logger.info("OK - Ping skipped, recorder {} is in offline mode.".format(recorder_agent.get('name')))
|
||||||
|
return True, "Recorder is in offline / maintenance mode", recorder_agent.get('name')
|
||||||
|
recorder_ip = get_recorder_by_name(recorder_agent.get('name')).get('ip')
|
||||||
|
try:
|
||||||
|
subprocess.check_call(
|
||||||
|
['ping', '-W', '10', '-c', '2', recorder_ip],
|
||||||
|
# stderr=subprocess.STDOUT, # get all output
|
||||||
|
stdout=subprocess.DEVNULL, # suppress output
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
universal_newlines=True # return string not bytes
|
||||||
|
)
|
||||||
|
logger.info("Successfully pinged {} ({}). :-)".format(recorder_agent.get('name'), recorder_ip))
|
||||||
|
return True, "Successfully pinged {}. :-)".format(recorder_agent.get('name')), recorder_agent.get('name')
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logger.error("Can not ping {} ({})!!".format(recorder_agent.get('name'), recorder_ip))
|
||||||
|
return False, "Unable to ping {} ({})".format(recorder_agent.get('name'), recorder_ip), recorder_agent.get(
|
||||||
|
'name')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
agents = get_capture_agents()
|
||||||
|
logger.info("Got {} capture agents that will be checked...".format(len(agents)))
|
||||||
|
|
||||||
|
for a in agents:
|
||||||
|
agent_states[a.get('name')] = 'PROBLEMATIC - unknown'
|
||||||
|
|
||||||
|
# pool = ThreadPool(5)
|
||||||
|
# pool.map(check_capture_agent_state, agents)
|
||||||
|
|
||||||
|
NUM_THREADS = 8
|
||||||
|
|
||||||
|
recorders = get_recorder_details()
|
||||||
|
with ThreadPool(NUM_THREADS) as pool:
|
||||||
|
# results = [pool.apply_async(ping_capture_agent, (agent,)) for agent in agents]
|
||||||
|
results = [pool.apply_async(ping_capture_agent, (agent,)) for agent in recorders]
|
||||||
|
try:
|
||||||
|
[res.get(timeout=12) for res in results]
|
||||||
|
except TimeoutError as e:
|
||||||
|
logger.error("Timeout while pinging capture agent! {}".format(e))
|
||||||
|
|
||||||
|
with ThreadPool(NUM_THREADS) as pool:
|
||||||
|
results = [pool.apply_async(check_capture_agent_state, (agent,)) for agent in agents]
|
||||||
|
try:
|
||||||
|
[res.get(timeout=12) for res in results]
|
||||||
|
except TimeoutError as e:
|
||||||
|
logger.error("Timeout while getting capture agent state! {}".format(e))
|
||||||
|
|
||||||
|
logger.info("DONE checking capture agents / recorders!")
|
||||||
|
|
||||||
|
logged_events = rec_err_state_log_stream.getvalue()
|
||||||
|
if len(logged_events) > 0:
|
||||||
|
logged_events += "\n\n=============\nAgent States:\n\n{}".format(''.join(
|
||||||
|
"{:<48}: {}\n".format(a, agent_states[a]) for a in agent_states
|
||||||
|
))
|
||||||
|
send_error_mail(logged_events, "Errors have been detected while checking recorder states!")
|
||||||
|
|
||||||
|
# mem_handler.close()
|
||||||
119
backend/tools/recorder_streams_sanity_checks.py
Normal file
119
backend/tools/recorder_streams_sanity_checks.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import ffmpeg
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from PIL import Image
|
||||||
|
from pydub import AudioSegment
|
||||||
|
from pydub.playback import play
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.tools.recorder_streams_sanity_checks")
|
||||||
|
|
||||||
|
|
||||||
|
def is_single_color_image(image):
|
||||||
|
single_color_image = True
|
||||||
|
color = {}
|
||||||
|
count = 0
|
||||||
|
color_channels = image.split()
|
||||||
|
for c in color_channels: # r, g, b
|
||||||
|
hist = c.histogram()
|
||||||
|
num_of_non_zero_values = len([v for v in hist if v != 0])
|
||||||
|
logger.debug("color_channel: {}, num_of_non_zero_values (NON-BLACK): {}".format(c, num_of_non_zero_values))
|
||||||
|
if num_of_non_zero_values > 1:
|
||||||
|
single_color_image = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
color[count] = [i for i in enumerate(hist) if i[1] != 0][0][0]
|
||||||
|
count += 1
|
||||||
|
return single_color_image, color
|
||||||
|
|
||||||
|
|
||||||
|
def check_frame_is_valid(stream_url, raise_errors=True):
|
||||||
|
try:
|
||||||
|
frame, _ = (
|
||||||
|
ffmpeg
|
||||||
|
.input(stream_url)
|
||||||
|
.output('pipe:', vframes=1, format='image2', vcodec='mjpeg')
|
||||||
|
.run(capture_stdout=True, capture_stderr=True)
|
||||||
|
)
|
||||||
|
image = Image.open(io.BytesIO(frame))
|
||||||
|
single_color_image, color = is_single_color_image(image)
|
||||||
|
if not single_color_image:
|
||||||
|
image.show()
|
||||||
|
return True, "all good :-)"
|
||||||
|
else:
|
||||||
|
if all(map(lambda x: x == 0, color.values())):
|
||||||
|
return False, "Image is entirely black! (color: {} (RGB))".format(
|
||||||
|
':'.join([str(x) for x in color.values()]))
|
||||||
|
return False, "Image has only one single color! (color: {} (RGB))".format(
|
||||||
|
':'.join([str(x) for x in color.values()]))
|
||||||
|
except ffmpeg.Error as e:
|
||||||
|
msg = "Could not connect to stream URL or other error!"
|
||||||
|
logger.error(msg)
|
||||||
|
try:
|
||||||
|
msg += " ({})".format(e.stderr.decode('utf-8').strip().split("\n")[-1])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
if raise_errors:
|
||||||
|
raise Exception(msg)
|
||||||
|
else:
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_audio_is_valid(stream_url, sample_length_sec=3, lower_alert_limit_dBFS=-40.0, raise_errors=True):
|
||||||
|
file_name = tempfile.NamedTemporaryFile(suffix='.aac').name
|
||||||
|
if os.path.exists(file_name):
|
||||||
|
os.remove(file_name)
|
||||||
|
try:
|
||||||
|
frame, _ = (
|
||||||
|
ffmpeg
|
||||||
|
.input(stream_url, t=sample_length_sec)
|
||||||
|
.output(file_name)
|
||||||
|
.run(capture_stdout=True, capture_stderr=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
sound = AudioSegment.from_file(file_name, "aac")
|
||||||
|
# print(sound.dBFS)
|
||||||
|
# play(sound)
|
||||||
|
if sound.max_dBFS == -float('inf'):
|
||||||
|
return False, "No active audio signal detected!"
|
||||||
|
elif sound.max_dBFS < lower_alert_limit_dBFS:
|
||||||
|
return False, "Very low volume (< {} dBFS) detected! ({})".format(lower_alert_limit_dBFS, sound.max_dBFS)
|
||||||
|
|
||||||
|
return True, "good audio signal detected! ({} max dBFS in {}s sample)".format(sound.max_dBFS, sample_length_sec)
|
||||||
|
except ffmpeg.Error as e:
|
||||||
|
msg = "Could not connect to stream URL or other error!"
|
||||||
|
logger.error(msg)
|
||||||
|
try:
|
||||||
|
msg += " ({})".format(e.stderr.decode('utf-8').strip().split("\n")[-1])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
if raise_errors:
|
||||||
|
raise Exception(msg)
|
||||||
|
else:
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Following code is not working correctly - ffmpeg parameters are wrong.
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
def check_if_audio_is_valid_stream(stream_url, raise_errors=True):
|
||||||
|
audio, _ = (
|
||||||
|
ffmpeg
|
||||||
|
.input(stream_url, t=2)
|
||||||
|
.output('pipe:', f="adts", acodec='copy')
|
||||||
|
.run(capture_stdout=False, capture_stderr=False)
|
||||||
|
)
|
||||||
|
audio = io.BytesIO(audio)
|
||||||
|
sound = AudioSegment.from_file(audio, "aac")
|
||||||
|
play(sound)
|
||||||
|
|
||||||
|
# check_if_audio_is_valid('rtsp://172.22.246.207/extron1')
|
||||||
|
"""
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# print(check_frame_is_valid('rtsp://172.22.246.207/extron2'))
|
||||||
|
print(check_if_audio_is_valid('rtsp://172.22.246.207/extron1'))
|
||||||
60
backend/tools/scrape_rooms.py
Normal file
60
backend/tools/scrape_rooms.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_rooms():
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}
|
||||||
|
|
||||||
|
room_url = "https://campus.kit.edu/live-stud/campus/all/roomgroup.asp?roomgroupcolumn1=H%F6r%2D%2FLehrsaal&tguid=0x1A35C3A1490748388EBEBA3943EFCDD5"
|
||||||
|
page = requests.get(room_url, headers=headers)
|
||||||
|
# soup = BeautifulSoup(page.content, 'html5lib')
|
||||||
|
soup = BeautifulSoup(page.content, 'html.parser')
|
||||||
|
|
||||||
|
# pprint(page.content)
|
||||||
|
|
||||||
|
# pprint(soup.prettify())
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
re_string = r"^(\d\d.\d\d)?\s(.*)"
|
||||||
|
re_exp = re.compile(re_string)
|
||||||
|
|
||||||
|
for tr in soup.find_all('tr'):
|
||||||
|
idx += 1
|
||||||
|
if idx == 1: # skip first row
|
||||||
|
continue
|
||||||
|
a_name = tr.find_all('a')[0].string
|
||||||
|
a_building = tr.find_all('a')[3].string
|
||||||
|
match = re_exp.match(a_name)
|
||||||
|
if match is not None:
|
||||||
|
building_number, name = re_exp.match(a_name).groups()
|
||||||
|
else:
|
||||||
|
name = a_name
|
||||||
|
building_number = None
|
||||||
|
|
||||||
|
match = re_exp.match(a_building)
|
||||||
|
if match is not None:
|
||||||
|
building_number, building_name = re_exp.match(a_building).groups()
|
||||||
|
else:
|
||||||
|
building_name = a_name
|
||||||
|
building_number = None
|
||||||
|
|
||||||
|
room = {'name': name,
|
||||||
|
'room_number': tr.find_all('a')[1].string if tr.find_all('a')[0].string != "None" else tr.find_all('a')[
|
||||||
|
1].string,
|
||||||
|
'building_name': building_name,
|
||||||
|
'building_number': building_number}
|
||||||
|
|
||||||
|
rooms.append(room)
|
||||||
|
|
||||||
|
return rooms
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
pprint(scrape_rooms())
|
||||||
60
backend/tools/send_mail.py
Normal file
60
backend/tools/send_mail.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import smtplib
|
||||||
|
import traceback
|
||||||
|
from email.message import EmailMessage
|
||||||
|
from logging.handlers import SMTPHandler
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from backend import Config
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(subject: str, msg_body: str, to_mail: List, from_mail: str = Config.FROM_MAIL):
|
||||||
|
subject = subject.strip().replace("\r", "").split("\n")[0] # remove breaks resp. cut after line break
|
||||||
|
try:
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg.set_content(msg_body)
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = from_mail
|
||||||
|
msg["To"] = to_mail
|
||||||
|
s = smtplib.SMTP(Config.SMTP_SERVER)
|
||||||
|
s.send_message(msg)
|
||||||
|
s.quit()
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
"Could not send E-Mail (Exception: {})".format(str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
def send_error_mail(message_or_exception: Union[str, Exception], subject: str = None,
|
||||||
|
subject_prefix: str = "[LRC] Error: ",
|
||||||
|
receiver_mail_addresses=Config.ERROR_E_MAIL_TO):
|
||||||
|
if isinstance(message_or_exception, Exception):
|
||||||
|
if subject is None:
|
||||||
|
subject = subject_prefix + str(type(message_or_exception))
|
||||||
|
else:
|
||||||
|
subject = subject_prefix + subject
|
||||||
|
msg_body = str(type(message_or_exception)) + ": " + str(message_or_exception) + "\n\n" + traceback.format_exc()
|
||||||
|
else:
|
||||||
|
if subject is None:
|
||||||
|
subject = str(message_or_exception)
|
||||||
|
else:
|
||||||
|
subject = subject_prefix + subject
|
||||||
|
msg_body = str(message_or_exception)
|
||||||
|
|
||||||
|
send_mail(subject, msg_body, receiver_mail_addresses)
|
||||||
|
|
||||||
|
|
||||||
|
def send_warning_mail():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_smtp_default_handler(receiver_mail_addresses=Config.ADMIN_E_MAIL_TO, subject: str = None,
|
||||||
|
subject_prefix: str = "[LRC] Log: "):
|
||||||
|
|
||||||
|
subject = subject if subject.startswith("[LRC]") else subject_prefix + subject
|
||||||
|
return SMTPHandler(Config.SMTP_SERVER, Config.FROM_MAIL, receiver_mail_addresses, subject)
|
||||||
|
|
||||||
|
|
||||||
|
def get_smtp_error_handler(receiver_mail_addresses=Config.ERROR_E_MAIL_TO, subject: str = None,
|
||||||
|
subject_prefix: str = "[LRC] Error: "):
|
||||||
|
|
||||||
|
subject = subject if subject.startswith("[LRC]") else subject_prefix + subject
|
||||||
|
return SMTPHandler(Config.SMTP_SERVER, Config.FROM_MAIL, receiver_mail_addresses, subject)
|
||||||
4
backend/websocket/__init__.py
Normal file
4
backend/websocket/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
|
||||||
|
import backend.websocket.handlers
|
||||||
|
|
||||||
92
backend/websocket/base.py
Normal file
92
backend/websocket/base.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from flask_jwt_extended import verify_jwt_in_request, get_current_user, jwt_required, get_jwt_claims, get_jwt_identity
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_socketio import SocketIO, emit
|
||||||
|
from jwt import ExpiredSignatureError
|
||||||
|
|
||||||
|
from backend import app
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.websocket")
|
||||||
|
|
||||||
|
async_mode = 'threading' # set to traditional python threading model
|
||||||
|
# async_mode = None
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode)
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketBase:
|
||||||
|
def __init__(self, flask_app_context=None):
|
||||||
|
if flask_app_context is None:
|
||||||
|
self.flask_app_context = app
|
||||||
|
self.socket_thread = None
|
||||||
|
|
||||||
|
def start_websocket_in_thread(self, host=None, port=None, debug=None, **kwargs):
|
||||||
|
self.socket_thread = threading.Thread(
|
||||||
|
target=self.start_websocket,
|
||||||
|
args=(host, port, debug, kwargs))
|
||||||
|
self.socket_thread.start()
|
||||||
|
return self.socket_thread
|
||||||
|
|
||||||
|
def start_websocket(self, host=None, port=None, debug=None, **kwargs):
|
||||||
|
if debug is None:
|
||||||
|
debug = self.flask_app_context.debug
|
||||||
|
socketio.run(self.flask_app_context, host=host, port=port, debug=debug, **kwargs)
|
||||||
|
|
||||||
|
def send_test_msg(self):
|
||||||
|
socketio.emit('test', "tolle nachricht")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@socketio.on('connect')
|
||||||
|
def connect_handler():
|
||||||
|
logger.debug("new connection...")
|
||||||
|
try:
|
||||||
|
print(verify_jwt_in_request())
|
||||||
|
print(get_jwt_identity())
|
||||||
|
except ExpiredSignatureError:
|
||||||
|
logger.info("user is not authenticated! Signature expired.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("user is not authenticated!")
|
||||||
|
print(str(e))
|
||||||
|
print(type(e))
|
||||||
|
print("not allowed!!")
|
||||||
|
return False # not allowed here
|
||||||
|
logger.debug("user is authenticated")
|
||||||
|
print("allowed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@socketio.on('message')
|
||||||
|
def handle_message(message):
|
||||||
|
print('received message: ' + message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@socketio.on('json')
|
||||||
|
def handle_json(json):
|
||||||
|
print('received json: ' + str(json))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _request_request_recorder_status_update():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _test_on_msg_func():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@socketio.on('update_message_test_blabla')
|
||||||
|
def handle_msg(msg=None):
|
||||||
|
print('received msg: ' + str(msg))
|
||||||
|
socketio.on_event('my event', WebSocketBase._test_on_msg_func, namespace='/')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@socketio.on_error()
|
||||||
|
def handle_error(error):
|
||||||
|
logger.error(error)
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
wsb = WebSocketBase()
|
||||||
|
# wsb.start_websocket_in_thread(debug=True)
|
||||||
|
wsb.start_websocket(debug=True)
|
||||||
50
backend/websocket/handlers.py
Normal file
50
backend/websocket/handlers.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from backend.cron import async_cron_recorder_checker
|
||||||
|
from backend.websocket.base import socketio
|
||||||
|
|
||||||
|
logger = logging.getLogger("lrc.websocket.handlers")
|
||||||
|
|
||||||
|
recorder_state_checker = async_cron_recorder_checker
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('request_recorder_state_updates')
|
||||||
|
def handle_request_recorder_state_updates_msg(recorder_id=None):
|
||||||
|
if recorder_id is None:
|
||||||
|
logger.warning("No recorder_id communicated, ignoring!")
|
||||||
|
return
|
||||||
|
logger.info("Adding recorder {} to state checker".format(recorder_id))
|
||||||
|
recorder_state_checker.add_object_to_state_check(recorder_id)
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('force_recorder_state_update')
|
||||||
|
def handle_force_recorder_state_update_msg(recorder_id=None):
|
||||||
|
if recorder_id is None:
|
||||||
|
logger.warning("No recorder_id communicated, ignoring!")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_states = recorder_state_checker.get_current_state()
|
||||||
|
for key in current_states:
|
||||||
|
state = current_states[key]
|
||||||
|
if state.get('id', None) == recorder_id:
|
||||||
|
logger.debug("Sending state to: {}".format(
|
||||||
|
'recorder_state_update_{}'.format(recorder_id)))
|
||||||
|
socketio.emit('recorder_state_update_{}'.format(recorder_id),
|
||||||
|
json.dumps(state))
|
||||||
|
return
|
||||||
|
logger.warning("Can't force update, no state found for recorder id {}.".format(recorder_id))
|
||||||
|
|
||||||
|
|
||||||
|
def send_state_update_to_recorders(recorder_results_dict: dict):
|
||||||
|
if len(recorder_results_dict) <= 0:
|
||||||
|
logger.debug("Sending state of recorders via web socket... => nothing to send!")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Sending state of recorders via web socket...")
|
||||||
|
for recorder_id in recorder_results_dict:
|
||||||
|
print(recorder_results_dict[recorder_id])
|
||||||
|
logger.debug("Sending state to: {}".format(
|
||||||
|
'recorder_state_update_{}'.format(recorder_results_dict[recorder_id].get('id'))))
|
||||||
|
socketio.emit('recorder_state_update_{}'.format(recorder_results_dict[recorder_id].get('id')),
|
||||||
|
json.dumps(recorder_results_dict[recorder_id]))
|
||||||
127
backend/websocket/websocket_base_testing.py
Normal file
127
backend/websocket/websocket_base_testing.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from flask import Flask, request
|
||||||
|
from flask_socketio import SocketIO, emit
|
||||||
|
|
||||||
|
from backend import app
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = 'secret!'
|
||||||
|
|
||||||
|
async_mode = 'threading'
|
||||||
|
async_mode = None
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode)
|
||||||
|
thread = None
|
||||||
|
thread_lock = threading.Lock()
|
||||||
|
|
||||||
|
clients = []
|
||||||
|
|
||||||
|
|
||||||
|
def background_thread():
|
||||||
|
"""Example of how to send server generated events to clients."""
|
||||||
|
count = 0
|
||||||
|
while True:
|
||||||
|
socketio.sleep(3)
|
||||||
|
print("server_event ")
|
||||||
|
count += 1
|
||||||
|
socketio.emit('server_event',
|
||||||
|
{'data': 'Server generated event', 'count': count},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ack():
|
||||||
|
print('message was received!')
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('my_event')
|
||||||
|
def test_message(message: dict):
|
||||||
|
print("received msg!")
|
||||||
|
print(message)
|
||||||
|
if isinstance(message, str):
|
||||||
|
emit('my_response', 'tolle Antwort! (got only a string!)', callback=ack)
|
||||||
|
else:
|
||||||
|
emit('my_response', {'data': message['data']}, callback=ack)
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('my broadcast event', namespace='/')
|
||||||
|
def test_message(message):
|
||||||
|
emit('my response', {'data': message['data']}, broadcast=True)
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('connect')
|
||||||
|
def test_connect():
|
||||||
|
print("connected")
|
||||||
|
global thread
|
||||||
|
with thread_lock:
|
||||||
|
if thread is None:
|
||||||
|
thread = socketio.start_background_task(background_thread)
|
||||||
|
emit('connect', "You are connected!")
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('disconnect', namespace='/')
|
||||||
|
def test_disconnect():
|
||||||
|
print('Client disconnected')
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on_error()
|
||||||
|
def handle_error(error):
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
|
||||||
|
def start_in_thread():
|
||||||
|
socket_thread = threading.Thread(target=lambda appl, host, port: socketio.run(app=appl, host=host, port=port),
|
||||||
|
args=(app, "localhost", 5000))
|
||||||
|
socket_thread.start()
|
||||||
|
return socket_thread
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def root():
|
||||||
|
return """<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
|
||||||
|
<script type="text/javascript" charset="utf-8">
|
||||||
|
var socket = io();
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log("connected!");
|
||||||
|
socket.emit('my_event', {data: 'I am connected!'});
|
||||||
|
});
|
||||||
|
socket.on('my_response', function(resp) {
|
||||||
|
console.log('got response:');
|
||||||
|
console.log(resp);
|
||||||
|
});
|
||||||
|
</script>"""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("running main")
|
||||||
|
# socketio.init_app(app, cors_allowed_origins="*")
|
||||||
|
|
||||||
|
#while len(clients) <= 0:
|
||||||
|
# time.sleep(1)
|
||||||
|
|
||||||
|
#while True:
|
||||||
|
# time.sleep(2)
|
||||||
|
# print("sending message...")
|
||||||
|
# socketio.send("lala")
|
||||||
|
# print("send lala")
|
||||||
|
# print(clients[0])
|
||||||
|
# socketio.emit("server_event", {'msg': "tolle nachricht"}, room=clients[0])
|
||||||
|
# with app.test_request_context('/'):
|
||||||
|
# socketio.emit('server_event', "You!: server_event", namespace="/")
|
||||||
|
# print("send bla")
|
||||||
|
|
||||||
|
|
||||||
|
#socketio.run(app, host="localhost", port=5000, debug=True)
|
||||||
|
start_in_thread()
|
||||||
|
while True:
|
||||||
|
time.sleep(2)
|
||||||
|
print("still running! :)")
|
||||||
|
# socketio.run(app, debug=True)
|
||||||
|
# ENDE
|
||||||
|
|
||||||
|
# t.join()
|
||||||
31
backend/websocket/websocket_test_client.py
Normal file
31
backend/websocket/websocket_test_client.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Copyright (c) 2019. Tobias Kurze
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from socketIO_client import SocketIO, LoggingNamespace
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
|
||||||
|
|
||||||
|
token = "# replace with: JWT Access Token"
|
||||||
|
#print(token)
|
||||||
|
|
||||||
|
print("params")
|
||||||
|
#socketIO = SocketIO('127.0.0.1', 5443, params={'jwt': '{}'.format(token)})
|
||||||
|
print("headers")
|
||||||
|
#socketIO = SocketIO('127.0.0.1', 5443, headers={'Authorization': 'Bearer {}'.format(token)})
|
||||||
|
print("cookies")
|
||||||
|
socketIO = SocketIO('127.0.0.1', 5443, cookies={'access_token_cookie': '{}'.format(token)})
|
||||||
|
|
||||||
|
#socketio = SocketIO(message_queue="redis://")
|
||||||
|
socketio = SocketIO('127.0.0.1', 5443)
|
||||||
|
|
||||||
|
#socketio.run(app, host="localhost", port=5000)
|
||||||
|
#socketio.init_app(app, host="localhost", port=5000, cors_allowed_origins="*", )
|
||||||
|
#socketio.init_app(app, host="localhost", port=5000, cors_allowed_origins="*", )
|
||||||
|
|
||||||
|
socketio.emit("server_event", {'data': 42, 'msg': 'toll'})
|
||||||
|
print("sent message!")
|
||||||
|
socketio.emit({'data': 42, 'msg': 'toll'})
|
||||||
|
print("sent message 2!")
|
||||||
1
current_rooms_cache.json
Normal file
1
current_rooms_cache.json
Normal file
File diff suppressed because one or more lines are too long
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
45
migrations/alembic.ini
Normal file
45
migrations/alembic.ini
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
96
migrations/env.py
Normal file
96
migrations/env.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
from flask import current_app
|
||||||
|
config.set_main_option(
|
||||||
|
'sqlalchemy.url',
|
||||||
|
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
|
||||||
|
target_metadata = current_app.extensions['migrate'].db.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 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 = config.get_main_option("sqlalchemy.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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
process_revision_directives=process_revision_directives,
|
||||||
|
**current_app.extensions['migrate'].configure_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${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 upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Example user model and related models
|
|
||||||
"""
|
|
||||||
|
|
||||||
from backend import db, app
|
|
||||||
from backend.models.post_model import Post
|
|
||||||
from backend.models.example_model import ExampleDataItem
|
|
||||||
import re
|
|
||||||
import jwt
|
|
||||||
from flask_login import UserMixin
|
|
||||||
from sqlalchemy import or_
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from passlib.hash import sha256_crypt
|
|
||||||
from hashlib import md5
|
|
||||||
|
|
||||||
followers = db.Table('followers',
|
|
||||||
db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
|
|
||||||
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
|
|
||||||
)
|
|
||||||
|
|
||||||
acquaintances = db.Table('acquaintances',
|
|
||||||
db.Column('me_id', db.Integer, db.ForeignKey('user.id')),
|
|
||||||
db.Column('acquaintance_id', db.Integer, db.ForeignKey('user.id'))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
|
||||||
"""
|
|
||||||
Example user model representation.
|
|
||||||
"""
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
social_id = db.Column(db.String(64), nullable=True, unique=True)
|
|
||||||
nickname = db.Column(db.String(64), index=True, unique=True)
|
|
||||||
first_name = db.Column(db.String(64), index=True, nullable=True)
|
|
||||||
last_name = db.Column(db.String(64), index=True, nullable=True)
|
|
||||||
email = db.Column(db.String(120), nullable=False, index=True, unique=True)
|
|
||||||
lang = db.Column(db.String(16), index=False, unique=False)
|
|
||||||
timezone = db.Column(db.String(32), index=False, unique=False)
|
|
||||||
posts = db.relationship('Post', backref='author', lazy='dynamic')
|
|
||||||
example_data_item = db.relationship('ExampleDataItem', backref='owner', lazy='dynamic')
|
|
||||||
about_me = db.Column(db.String(140))
|
|
||||||
role = db.Column(db.String(64))
|
|
||||||
password = db.Column(db.String(255), nullable=True)
|
|
||||||
registered_on = db.Column(db.DateTime, nullable=False, default=datetime.utcnow())
|
|
||||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow())
|
|
||||||
jwt_exp_delta_seconds = db.Column(db.Integer, nullable=True)
|
|
||||||
acquainted = db.relationship('User',
|
|
||||||
secondary=acquaintances,
|
|
||||||
primaryjoin=(acquaintances.c.me_id == id),
|
|
||||||
secondaryjoin=(acquaintances.c.acquaintance_id == id),
|
|
||||||
backref=db.backref('acquaintances', lazy='dynamic'),
|
|
||||||
lazy='dynamic')
|
|
||||||
followed = db.relationship('User',
|
|
||||||
secondary=followers,
|
|
||||||
primaryjoin=(followers.c.follower_id == id),
|
|
||||||
secondaryjoin=(followers.c.followed_id == id),
|
|
||||||
backref=db.backref('followers', lazy='dynamic'),
|
|
||||||
lazy='dynamic')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_by_identifier(identifier):
|
|
||||||
"""
|
|
||||||
Find user by identifier, which might be the nickname or the e-mail address.
|
|
||||||
:param identifier:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return User.query.filter(or_(User.nickname == identifier,
|
|
||||||
User.email == identifier)).first()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all():
|
|
||||||
"""
|
|
||||||
Return all users
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return User.query.all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def make_unique_nickname(nickname):
|
|
||||||
"""
|
|
||||||
Add suffix (counter) to nickname in order to get a unique nickname.
|
|
||||||
:param nickname:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if User.query.filter_by(nickname=nickname).first() is None:
|
|
||||||
return nickname
|
|
||||||
version = 2
|
|
||||||
while True:
|
|
||||||
new_nickname = nickname + str(version)
|
|
||||||
if User.query.filter_by(nickname=new_nickname).first() is None:
|
|
||||||
break
|
|
||||||
version += 1
|
|
||||||
return new_nickname
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def make_valid_nickname(nickname):
|
|
||||||
"""
|
|
||||||
Replaces certain characters (except a-zA-Z0-9_.) in nickname with blancs.
|
|
||||||
:param nickname:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return re.sub('[^a-zA-Z0-9_.]', '', nickname)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_authenticated(self):
|
|
||||||
"""
|
|
||||||
Returns true if user is authenticated.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# TODO: implement correctly
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_active(self):
|
|
||||||
"""
|
|
||||||
Returns true if user is active.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# TODO: implement correctly
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_anonymous(self):
|
|
||||||
"""
|
|
||||||
Returns true if user is anonymous.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# TODO: implement correctly
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def decode_auth_token(auth_token):
|
|
||||||
"""
|
|
||||||
Decodes the auth token
|
|
||||||
:param auth_token:
|
|
||||||
:return: integer|string
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(auth_token, app.config.get('SECRET_KEY'))
|
|
||||||
is_blacklisted_token = BlacklistToken.check_blacklist(auth_token)
|
|
||||||
if is_blacklisted_token:
|
|
||||||
return 'Token blacklisted. Please log in again.'
|
|
||||||
else:
|
|
||||||
return payload['sub']
|
|
||||||
except jwt.ExpiredSignatureError:
|
|
||||||
return 'Signature expired. Please log in again.'
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
return 'Invalid token. Please log in again.'
|
|
||||||
|
|
||||||
def encode_auth_token(self):
|
|
||||||
"""
|
|
||||||
Generates the Auth Token
|
|
||||||
:return: string
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
payload = {
|
|
||||||
'exp': datetime.utcnow() + timedelta(days=0, hours=3, seconds=5),
|
|
||||||
'iat': datetime.utcnow(),
|
|
||||||
'sub': self.id
|
|
||||||
}
|
|
||||||
return jwt.encode(
|
|
||||||
payload,
|
|
||||||
app.config.get('SECRET_KEY'),
|
|
||||||
algorithm='HS256'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return e
|
|
||||||
|
|
||||||
def set_password(self, password):
|
|
||||||
"""
|
|
||||||
SHA256 encrypts the given password and sets it on the user.
|
|
||||||
:param password:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
self.password = sha256_crypt.encrypt(password)
|
|
||||||
|
|
||||||
def verify_password(self, password):
|
|
||||||
"""
|
|
||||||
Verifies that the given password matches the SHA256 encrypted password stored on the user.
|
|
||||||
:param password:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return sha256_crypt.verify(password, self.password)
|
|
||||||
|
|
||||||
def get_id(self):
|
|
||||||
"""
|
|
||||||
Returns the ID of the user.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
return unicode(self.id) # python 2
|
|
||||||
except NameError:
|
|
||||||
return str(self.id) # python 3
|
|
||||||
|
|
||||||
def avatar(self, size):
|
|
||||||
"""
|
|
||||||
Returns an avatar URL.
|
|
||||||
:param size:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return 'https://s.gravatar.com/avatar/%s?d=mm&s=%d' % (md5(self.email.encode('utf-8')).hexdigest(), size)
|
|
||||||
|
|
||||||
def acquaint(self, user):
|
|
||||||
"""
|
|
||||||
Adds an acquaintance to the user object.
|
|
||||||
:param user:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not self.is_acquainted(user):
|
|
||||||
self.acquainted.append(user)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unacquaint(self, user):
|
|
||||||
"""
|
|
||||||
Removes the user from the list of acquaintances.
|
|
||||||
:param user:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self.is_acquainted(user):
|
|
||||||
self.acquainted.remove(user)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def is_acquainted(self, user):
|
|
||||||
"""
|
|
||||||
Check if the provided user is an acquaintance.
|
|
||||||
:param user:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return self.acquainted.filter(acquaintances.c.acquaintance_id == user.id).count() > 0
|
|
||||||
|
|
||||||
def get_acquaintances(self):
|
|
||||||
"""
|
|
||||||
Returns the list of acquaintances.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return User.query.join(acquaintances, (acquaintances.c.acquaintance_id == User.id)).filter(
|
|
||||||
acquaintances.c.me_id == self.id).order_by(User.nickname.desc())
|
|
||||||
|
|
||||||
def shared_example_data_items(self):
|
|
||||||
"""
|
|
||||||
Returns a list of the shared data items.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return ExampleDataItem.query.join(acquaintances,
|
|
||||||
(acquaintances.c.acquaintance_id == ExampleDataItem.user_id)).filter(
|
|
||||||
acquaintances.c.me_id == self.id).order_by(ExampleDataItem.timestamp.desc())
|
|
||||||
|
|
||||||
def follow(self, user):
|
|
||||||
"""
|
|
||||||
Add user to list of followers.
|
|
||||||
:param user:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not self.is_following(user):
|
|
||||||
self.followed.append(user)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def unfollow(self, user):
|
|
||||||
"""
|
|
||||||
Remove user from the list of followers.
|
|
||||||
:param user:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self.is_following(user):
|
|
||||||
self.followed.remove(user)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def is_following(self, user):
|
|
||||||
"""
|
|
||||||
Checks if specified user is a follower.
|
|
||||||
:param user:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return self.followed.filter(followers.c.followed_id == user.id).count() > 0
|
|
||||||
|
|
||||||
def followed_posts(self):
|
|
||||||
"""
|
|
||||||
Returns list of followed posts.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return Post.query.join(followers, (followers.c.followed_id == Post.user_id)).filter(
|
|
||||||
followers.c.follower_id == self.id).order_by(Post.timestamp.desc())
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<User %r>' % self.nickname
|
|
||||||
|
|
||||||
|
|
||||||
class BlacklistToken(db.Model):
|
|
||||||
"""
|
|
||||||
Token Model for storing JWT tokens
|
|
||||||
"""
|
|
||||||
__tablename__ = 'blacklist_tokens'
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
|
||||||
token = db.Column(db.String(500), unique=True, nullable=False)
|
|
||||||
blacklisted_on = db.Column(db.DateTime, nullable=False)
|
|
||||||
|
|
||||||
def __init__(self, token):
|
|
||||||
self.token = token
|
|
||||||
self.blacklisted_on = datetime.now()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<id: token: {}'.format(self.token)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_blacklist(auth_token):
|
|
||||||
"""
|
|
||||||
check whether auth token has been blacklisted
|
|
||||||
:param auth_token:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
res = BlacklistToken.query.filter_by(token=str(auth_token)).first()
|
|
||||||
if res:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
2485
poetry.lock
generated
Normal file
2485
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
pyproject.toml
Normal file
53
pyproject.toml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "lrc-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Tobias K. <kurze@kit.edu>"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.11"
|
||||||
|
ffmpeg-python = "*"
|
||||||
|
flask = "*"
|
||||||
|
flask-httpauth = "*"
|
||||||
|
flask-sqlalchemy = "*"
|
||||||
|
flask-login = "*"
|
||||||
|
pyjwt = "*"
|
||||||
|
passlib = "*"
|
||||||
|
sqlalchemy = "*"
|
||||||
|
sqlalchemy-migrate = "*"
|
||||||
|
flask-script = "*"
|
||||||
|
flask-migrate = "*"
|
||||||
|
coverage = "*"
|
||||||
|
flask-testing = "*"
|
||||||
|
flask-pyoidc = "*"
|
||||||
|
python-jose = "*"
|
||||||
|
flask-jwt-extended = "*"
|
||||||
|
ssh2-python = "*"
|
||||||
|
update = "*"
|
||||||
|
flask-cors = "*"
|
||||||
|
html5lib = "*"
|
||||||
|
beautifulsoup4 = "*"
|
||||||
|
flask-socketio = "*"
|
||||||
|
eventlet = "*"
|
||||||
|
ics = "*"
|
||||||
|
coloredlogs = "*"
|
||||||
|
pythonping = "*"
|
||||||
|
scapy = "*"
|
||||||
|
python-socketio = {version = "*", extras = ["client"]}
|
||||||
|
socketio-client = "*"
|
||||||
|
websocket-client = "*"
|
||||||
|
apscheduler = "*"
|
||||||
|
pillow = "*"
|
||||||
|
pydub = "*"
|
||||||
|
simpleaudio = "*"
|
||||||
|
flask-restx = "*"
|
||||||
|
campus_api_client = {git = "git@gitlab.kit.edu:kit/bib/it/tobias/campus_api_client.git"}
|
||||||
|
#cac = {git = "git@gitlab.kit.edu:kit/bib/it/tobias/campus_api_client.git"}
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
from flask import render_template, send_from_directory, Blueprint
|
|
||||||
|
|
||||||
fe_path = os.path.join(os.getcwd(), "frontend", "dist")
|
|
||||||
fe_bp = Blueprint('frontend', __name__, url_prefix='/', template_folder=os.path.join(fe_path, ""))
|
|
||||||
|
|
||||||
|
|
||||||
@fe_bp.route('/js/<path:path>')
|
|
||||||
def send_js(path):
|
|
||||||
return send_from_directory(os.path.join(fe_path, "js"), path)
|
|
||||||
|
|
||||||
|
|
||||||
@fe_bp.route('/css/<path:path>')
|
|
||||||
def send_css(path):
|
|
||||||
return send_from_directory(os.path.join(fe_path, "css"), path)
|
|
||||||
|
|
||||||
|
|
||||||
@fe_bp.route('/img/<path:path>')
|
|
||||||
def send_img(path):
|
|
||||||
return send_from_directory(os.path.join(fe_path, "img"), path)
|
|
||||||
|
|
||||||
|
|
||||||
@fe_bp.route('/', defaults={'path': ''})
|
|
||||||
@fe_bp.route('/<path:path>')
|
|
||||||
def catch_all(path):
|
|
||||||
return render_template("index.html")
|
|
||||||
Reference in New Issue
Block a user