moved everything to a new module called backend
This commit is contained in:
70
backend/__init__.py
Normal file
70
backend/__init__.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Backend base module
|
||||
"""
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from flask import Flask, jsonify
|
||||
from flask_httpauth import HTTPTokenAuth, HTTPBasicAuth, MultiAuth
|
||||
from flask_jwt_extended import JWTManager, decode_token
|
||||
from flask_login import LoginManager
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('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)
|
||||
#
|
||||
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)
|
||||
|
||||
# Fix flask-restplus by duck typing error handlers
|
||||
jwt_extended._set_error_handler_callbacks(api_v1)
|
||||
|
||||
41
backend/__main__.py
Normal file
41
backend/__main__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019. Tobias Kurze
|
||||
import logging
|
||||
import ssl
|
||||
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from backend import app, db
|
||||
from backend.models import pre_fill_table
|
||||
from backend.tools.model_updater import update_recorder_models_database
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
print(app.config.get("SERVER_NAME", None))
|
||||
server_name = app.config.get("SERVER_NAME", None)
|
||||
if server_name is not None and "ubkaps154.ubka.uni-karlsruhe.de" in server_name:
|
||||
try:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
|
||||
context.load_cert_chain('cert.pem', 'key.pem')
|
||||
app.run(debug=True, ssl_context=context, threaded=True)
|
||||
except FileNotFoundError:
|
||||
app.run(debug=True, threaded=True)
|
||||
|
||||
try:
|
||||
db.create_all()
|
||||
except Exception as e:
|
||||
logging.critical(e)
|
||||
|
||||
pre_fill_table()
|
||||
update_recorder_models_database()
|
||||
|
||||
|
||||
app.run(debug=True, host="0.0.0.0")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
78
backend/api/__init__.py
Normal file
78
backend/api/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import Blueprint, abort
|
||||
from flask_restplus import Api, Namespace
|
||||
|
||||
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_room = Namespace('room', description="Room management namespace", authorizations=api_authorizations)
|
||||
api_recorder = Namespace('recorder', description="Recorder 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_room)
|
||||
api_v1.add_namespace(api_recorder)
|
||||
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 .group_api import *
|
||||
from .room_api import *
|
||||
from .recorder_api import *
|
||||
from .control_api import *
|
||||
from .virtual_command_api import *
|
||||
|
||||
|
||||
# from .group_api import *
|
||||
|
||||
|
||||
@api_bp.route('/<path:path>')
|
||||
def catch_all_api(path):
|
||||
"""
|
||||
Default 404 response for undefined paths in API.
|
||||
:param path:
|
||||
:return:
|
||||
"""
|
||||
abort(404)
|
||||
196
backend/api/auth_api.py
Normal file
196
backend/api/auth_api.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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
|
||||
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_restplus import Resource, fields
|
||||
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.auth import AUTH_PROVIDERS, oidc_auth
|
||||
from backend.models.user_model import User, Group, BlacklistToken
|
||||
|
||||
|
||||
@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_bp.route('/register', methods=('POST',))
|
||||
def register():
|
||||
data = request.get_json()
|
||||
user = User(**data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(user.to_dict()), 201
|
||||
|
||||
|
||||
@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, description='The user\'s e-mail address'),
|
||||
'password': fields.String(required=False, description='The group\'s name')
|
||||
}))
|
||||
class AuthProviders(Resource):
|
||||
def get(self):
|
||||
return register()
|
||||
|
||||
|
||||
@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'])
|
||||
@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:
|
||||
return None
|
||||
|
||||
user_groups = check_and_create_groups(groups=userinfo.get("memberOf", []))
|
||||
user = User.get_by_identifier(email)
|
||||
|
||||
if user is not None:
|
||||
app.logger.info("user found -> update user")
|
||||
pprint(user.to_dict())
|
||||
user.first_name = userinfo.get("given_name", "")
|
||||
user.last_name = userinfo.get("family_name", "")
|
||||
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)
|
||||
|
||||
app.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()
|
||||
def oidc(redirect_url=None):
|
||||
user = create_or_retrieve_user_from_userinfo(flask.session['userinfo'])
|
||||
if user is None:
|
||||
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)
|
||||
})
|
||||
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 = "/"
|
||||
app.logger.info("Token: {}".format(token))
|
||||
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)
|
||||
app.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
|
||||
|
||||
42
backend/api/control_api.py
Normal file
42
backend/api/control_api.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# 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_restplus import fields, Resource
|
||||
|
||||
from backend import db
|
||||
from backend.api import api_control, get_jwt_identity
|
||||
|
||||
control_command_response_model = api_control.model('Control Command Response', {
|
||||
'time': fields.DateTime(required=False, description='Creation date of the recorder'),
|
||||
'state': fields.String(min_length=3, required=True, description='The recorder\'s name'),
|
||||
'output': fields.String(required=False, description='The recorder\'s description'),
|
||||
'error': fields.String(required=False, description='The recorder\'s description'),
|
||||
})
|
||||
|
||||
|
||||
@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=json.dumps({'p1': 'v1'}), 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()}
|
||||
args = self.control_command_parser.parse_args()
|
||||
return {'time': datetime.utcnow(), 'output': args, 'state': current_user}
|
||||
134
backend/api/example_api.py
Normal file
134
backend/api/example_api.py
Normal file
@@ -0,0 +1,134 @@
|
||||
|
||||
import datetime
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
from random import *
|
||||
from flask import jsonify, Blueprint, request
|
||||
from flask_restplus import Resource, reqparse
|
||||
|
||||
from backend import basic_auth, multi_auth, db, jwt_auth
|
||||
from backend.api import api_v1, api_bp
|
||||
|
||||
|
||||
@api_bp.route('/random')
|
||||
def random_number():
|
||||
"""
|
||||
:return: a random number
|
||||
"""
|
||||
response = {
|
||||
'randomNumber': randint(1, 100)
|
||||
}
|
||||
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):
|
||||
"""
|
||||
This is a test class.
|
||||
"""
|
||||
def get(self):
|
||||
"""
|
||||
just a test!
|
||||
:return: Hello: World
|
||||
"""
|
||||
print(str(self))
|
||||
return {'hello': 'world'}
|
||||
|
||||
|
||||
api_v1.add_resource(HelloWorld, '/')
|
||||
|
||||
|
||||
class SensorData_Handler(Resource):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('values', type=str, required=True, help="sensor data values are required (as JSON list)")
|
||||
|
||||
@basic_auth.login_required
|
||||
def get(self, mac):
|
||||
sensor = Sensor.get_by_mac_user(mac, g.user)
|
||||
if not sensor:
|
||||
return "sensor not found", 404
|
||||
return jsonify(sensor.get_sensor_data())
|
||||
# return {'task': 'id: ' + mac}
|
||||
|
||||
@multi_auth.login_required
|
||||
def post(self, mac):
|
||||
sensor = Sensor.get_by_mac(mac)
|
||||
if not sensor:
|
||||
return "sensor not found", 404
|
||||
|
||||
print("JSON")
|
||||
print(request.json)
|
||||
print(request.json['values'])
|
||||
args = SensorData_Handler.parser.parse_args()
|
||||
print("values...")
|
||||
print(args['values'])
|
||||
values = json.loads(args['values'])
|
||||
app.logger.info("vals: " + str(values) + " (len: " + str(len(values)) + ")")
|
||||
|
||||
rough_geo_location = None
|
||||
try:
|
||||
rough_geo_location_info = None
|
||||
ip = ipaddress.ip_address(request.remote_addr)
|
||||
if request.remote_addr == "127.0.0.1":
|
||||
ip = ipaddress.ip_address("89.245.37.108")
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
rough_geo_location_info = geoip4.record_by_addr(str(ip))
|
||||
elif isinstance(ip, ipaddress.IPv6Address):
|
||||
rough_geo_location_info = geoip6.record_by_addr(str(ip))
|
||||
|
||||
if rough_geo_location_info is not None:
|
||||
rough_geo_location = json.dumps({key: rough_geo_location_info[key] for key in ['country_code',
|
||||
'country_name',
|
||||
'postal_code',
|
||||
'city',
|
||||
'time_zone',
|
||||
'latitude',
|
||||
'longitude']
|
||||
})
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
for values_at_time in values:
|
||||
wasss_app.logger.info("values_at_time: " + str(values_at_time) + " (len: " + str(len(values_at_time)) + ")")
|
||||
sensor_data = SensorData(sensor=sensor, rough_geo_location=rough_geo_location)
|
||||
val_cycle = cycle(values_at_time)
|
||||
for sensor_data_header in sensor.get_sensor_data_headers():
|
||||
value = next(val_cycle)
|
||||
if sensor_data_header == SensorData.HEADER_TIMESTAMP:
|
||||
sensor_data.timestamp = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S%z")
|
||||
wasss_app.logger.info(sensor_data.timestamp)
|
||||
elif sensor_data_header == SensorData.HEADER_VOLTAGE:
|
||||
sensor_data.voltage = value
|
||||
elif sensor_data_header == SensorData.HEADER_TEMPERATURE:
|
||||
sensor_data.temp = value
|
||||
elif sensor_data_header == SensorData.HEADER_HUMIDITY:
|
||||
sensor_data.humidity = value
|
||||
elif sensor_data_header == SensorData.HEADER_PRESSURE:
|
||||
sensor_data.pressure = value
|
||||
elif sensor_data_header == SensorData.HEADER_BRIGHTNESS:
|
||||
sensor_data.brightness = value
|
||||
elif sensor_data_header == SensorData.HEADER_SPEED:
|
||||
sensor_data.speed = value
|
||||
elif sensor_data_header == SensorData.HEADER_MOTION:
|
||||
sensor_data.motion = value
|
||||
else:
|
||||
gen_sen_data = GenericSensorData(sensor=sensor, name=sensor_data_header, value=value)
|
||||
db.session.add(sensor_data)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify("ok")
|
||||
|
||||
|
||||
api_v1.add_resource(SensorData_Handler, '/sensor/<string:mac>/data')
|
||||
87
backend/api/group_api.py
Normal file
87
backend/api/group_api.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# 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_restplus 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.')
|
||||
})
|
||||
|
||||
|
||||
@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
|
||||
275
backend/api/recorder_api.py
Normal file
275
backend/api/recorder_api.py
Normal file
@@ -0,0 +1,275 @@
|
||||
# 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 jwt_required
|
||||
from flask_restplus import fields, Resource, inputs
|
||||
|
||||
from backend import db, app
|
||||
from backend.api import api_recorder
|
||||
from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand
|
||||
from backend.models.room_model import Room
|
||||
import backend.recorder_adapters as r_a
|
||||
|
||||
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'),
|
||||
'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.'),
|
||||
'ip': fields.String(required=False, description='The recorder\'s IP 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")
|
||||
})
|
||||
|
||||
|
||||
# ==
|
||||
|
||||
@api_recorder.route('/<int:id>')
|
||||
@api_recorder.response(404, 'Recorder not found')
|
||||
@api_recorder.param('id', 'The recorder identifier')
|
||||
class RecorderResource(Resource):
|
||||
@jwt_required
|
||||
@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
|
||||
@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
|
||||
@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
|
||||
@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
|
||||
@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
|
||||
recorder = Recorder(**api_recorder.payload)
|
||||
db.session.add(recorder)
|
||||
db.session.commit()
|
||||
return recorder
|
||||
|
||||
|
||||
@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
|
||||
@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
|
||||
@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
|
||||
@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
|
||||
@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()
|
||||
122
backend/api/room_api.py
Normal file
122
backend/api/room_api.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# 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_restplus import fields, Resource
|
||||
from sqlalchemy import exc
|
||||
|
||||
from backend import db, app
|
||||
from backend.api import api_room
|
||||
from backend.models.room_model import Room
|
||||
from backend.models.recorder_model import Recorder
|
||||
|
||||
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"]
|
||||
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
|
||||
107
backend/api/user_api.py
Normal file
107
backend/api/user_api.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# 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_restplus import Resource, fields, inputs
|
||||
|
||||
from backend import db, app, jwt_auth
|
||||
from backend.api import api_user
|
||||
from backend.models.user_model import User, Group
|
||||
|
||||
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'),
|
||||
'groups': fields.List(
|
||||
fields.Nested(api_user.model('user_group', {'id': fields.Integer(), 'name': fields.String()})),
|
||||
required=False, description='Group memberships.'),
|
||||
})
|
||||
|
||||
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('')
|
||||
class UserList(Resource):
|
||||
"""
|
||||
This is a test class.
|
||||
"""
|
||||
|
||||
# @jwt_auth.login_required
|
||||
@jwt_required
|
||||
@api_user.doc('users')
|
||||
@api_user.marshal_list_with(user_model)
|
||||
def get(self):
|
||||
"""
|
||||
just a test!
|
||||
:return: Hello: World
|
||||
"""
|
||||
current_user = get_jwt_identity()
|
||||
app.logger.info(current_user)
|
||||
return User.get_all()
|
||||
|
||||
@jwt_required
|
||||
@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
|
||||
@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)
|
||||
|
||||
# api_user.add_resource(UserResource, '/')
|
||||
134
backend/api/virtual_command_api.py
Normal file
134
backend/api/virtual_command_api.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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_restplus import fields, Resource
|
||||
|
||||
from backend import db, app
|
||||
from backend.api import api_virtual_command
|
||||
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.')),
|
||||
'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"""
|
||||
recorder = Recorder.query.get(id)
|
||||
if recorder is not None:
|
||||
return recorder
|
||||
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 RecorderList(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 Recorder.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):
|
||||
if "room_id" in api_virtual_command.payload:
|
||||
if api_virtual_command.payload["room_id"] is None:
|
||||
api_virtual_command.payload["room"] = None
|
||||
else:
|
||||
room = Room.query.get(api_virtual_command.payload["room_id"])
|
||||
if room is not None:
|
||||
api_virtual_command.payload["room"] = room
|
||||
else:
|
||||
return "specified room (id: {}) does not exist!".format(api_virtual_command.payload["room_id"]), 404
|
||||
if "recorder_model_id" in api_virtual_command.payload:
|
||||
if api_virtual_command.payload["recorder_model_id"] is None:
|
||||
api_virtual_command.payload["recorder_model"] = None
|
||||
else:
|
||||
rec_model = RecorderModel.query.get(api_virtual_command.payload["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 = Recorder(**api_virtual_command.payload)
|
||||
db.session.add(recorder)
|
||||
db.session.commit()
|
||||
return recorder
|
||||
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"
|
||||
77
backend/auth/oidc.py
Normal file
77
backend/auth/oidc.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) 2019. Tobias Kurze
|
||||
"""
|
||||
OIDC login auth module
|
||||
"""
|
||||
|
||||
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:
|
||||
return None
|
||||
user = User.get_by_identifier(email)
|
||||
|
||||
if user is not None:
|
||||
app.logger.info("user found")
|
||||
#TODO: update user!
|
||||
return user
|
||||
|
||||
user = User(email=email, first_name=userinfo.get("given_name", ""),
|
||||
last_name=userinfo.get("family_name", ""))
|
||||
|
||||
app.logger.info("creating new user")
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
|
||||
@auth_bp.route('/oidc', methods=['GET'])
|
||||
@oidc_auth.oidc_auth()
|
||||
def oidc():
|
||||
user_session = UserSession(flask.session)
|
||||
app.logger.info(user_session.userinfo)
|
||||
user = create_or_retrieve_user_from_userinfo(user_session.userinfo)
|
||||
login_user(user)
|
||||
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'])
|
||||
def oidc_logout():
|
||||
oidc_auth.oidc_logout()
|
||||
return redirect('/')
|
||||
15
backend/auth/oidc_config.py
Normal file
15
backend/auth/oidc_config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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', '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>
|
||||
42
backend/auth/utils.py
Normal file
42
backend/auth/utils.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import flask_jwt_extended
|
||||
from flask_jwt_extended import jwt_optional, get_jwt_identity
|
||||
from functools import wraps
|
||||
|
||||
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 user.has_permission(permission_level):
|
||||
#for g in user.groups:
|
||||
# if g.permissions
|
||||
#TODO
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
# return FALSE
|
||||
#if not session.get('email'):
|
||||
# return redirect(url_for('users.login'))
|
||||
|
||||
#user = User.find_by_email(session['email'])
|
||||
#elif not user.allowed(access_level):
|
||||
# return redirect(url_for('users.profile', message="You do not have access to that page. Sorry!"))
|
||||
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
|
||||
113
backend/config.py
Normal file
113
backend/config.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# ...
|
||||
# available languages
|
||||
import os
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Config():
|
||||
# SERVER_NAME = "ubkaps154.ubka.uni-karlsruhe.de:5443"
|
||||
# SERVER_NAME = "localhost.dev"
|
||||
SERVER_NAME = "localhost:5443"
|
||||
# SERVER_NAME = "localhost"
|
||||
# SERVER_NAME = "localhost.localdomain"
|
||||
# PORT = 5443
|
||||
# PREFERRED_URL_SCHEME = 'https'
|
||||
|
||||
TEMPLATE_AUTO_RELOAD = True
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
WTF_CSRF_ENABLED = True
|
||||
SECRET_KEY = 'you-will-never-guess'
|
||||
OPENID_PROVIDERS = [
|
||||
{'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'},
|
||||
{'name': 'Yahoo', 'url': 'https://me.yahoo.com'},
|
||||
{'name': 'AOL', 'url': 'http://openid.aol.com/<username>'},
|
||||
{'name': 'Flickr', 'url': 'http://www.flickr.com/<username>'},
|
||||
{'name': 'MyOpenID', 'url': 'https://www.myopenid.com'}]
|
||||
|
||||
OAUTH_CREDENTIALS = {
|
||||
'facebook': {
|
||||
'id': '1198624176930248',
|
||||
'secret': '4fbc01d776834c1ffc89a5bed1cd97d0'
|
||||
},
|
||||
'twitter': {
|
||||
'id': '3RzWQclolxWZIMq5LJqzRZPTl',
|
||||
'secret': 'm9TEd58DSEtRrZHpz2EjrV9AhsBRxKMo8m3kuIZj3zLwzwIimt'
|
||||
},
|
||||
'google': {
|
||||
'id': '1084993305658-d9n88548ssrtmt5v6s2dne57i4qpviur.apps.googleusercontent.com',
|
||||
'secret': 'oNpvoAKMPMjRyiu5EDrmmX4X'
|
||||
},
|
||||
}
|
||||
|
||||
# mail server settings
|
||||
MAIL_SERVER = 'localhost'
|
||||
MAIL_PORT = 25
|
||||
MAIL_USERNAME = None
|
||||
MAIL_PASSWORD = None
|
||||
|
||||
# administrator list
|
||||
ADMINS = ['you@example.com']
|
||||
|
||||
# pagination
|
||||
POSTS_PER_PAGE = 5
|
||||
LOCKS_PER_PAGE = 8
|
||||
|
||||
LANGUAGES = {
|
||||
'en': 'English',
|
||||
'es': 'Español'
|
||||
}
|
||||
|
||||
# ASSETS_DEBUG = True
|
||||
|
||||
# JWT_SECRET = "abcxyz"
|
||||
# JWT_ALGORITHM = "HS256"
|
||||
# JWT_EXP_DELTA_SECONDS = 5 * 60
|
||||
|
||||
JWT_SECRET_KEY = "abcxyz"
|
||||
JWT_BLACKLIST_ENABLED = True
|
||||
JWT_BLACKLIST_TOKEN_CHECKS = ['access', 'refresh']
|
||||
|
||||
AUTH_RETURN_EXTERNAL_JWT = False
|
||||
|
||||
INDEX_TEMPLATE = "index.html"
|
||||
|
||||
# # INITIAL VALUES # #
|
||||
|
||||
PERMISSIONS = ["RECODER_NEW", "RECORDER_EDIT", "RECODER_SHOW", "RECORDER_DELETE",
|
||||
"RECORDER_COMMAND_EXECUTE", "RECORDER_COMMAND_EDIT_ACL",
|
||||
"VIRTUAL_COMMAND_CREATE", "VIRTUAL_COMMAND_EDIT", "VIRTUAL_COMMAND_SHOW", "VIRTUAL_COMMAND_DELETE",
|
||||
"CRON_JOB_CREATE", "CRON_JOB_EDIT", "CRON_JOB_SHOW", "CRON_JOB_DELETE"]
|
||||
|
||||
GROUPS = [ #{"name": "Admins",
|
||||
#"permissions": PERMISSIONS},
|
||||
{"name": "ZML"},
|
||||
{"name": "read_only"}]
|
||||
|
||||
USERS = [{"nickname": "admin",
|
||||
"first_name": "tobias",
|
||||
"last_name": "kurze",
|
||||
"email": "kurze@kit.edu",
|
||||
"role": "admin",
|
||||
"password": "admin"}
|
||||
]
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
DATABASE_URI = 'mysql://user@localhost/foo'
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db_debug')
|
||||
SERVER_NAME = "ubkaps154.ubka.uni-karlsruhe.de"
|
||||
PORT = 5443
|
||||
DEBUG = True
|
||||
|
||||
class TestingConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db_test')
|
||||
TESTING = True
|
||||
75
backend/manage.py
Normal file
75
backend/manage.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
import os, sys
|
||||
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
|
||||
|
||||
|
||||
@manager.command
|
||||
def create_db():
|
||||
"""Creates the db tables."""
|
||||
db.create_all()
|
||||
|
||||
|
||||
@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 ###
|
||||
9
backend/models/__init__.py
Normal file
9
backend/models/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Import all models...
|
||||
"""
|
||||
from backend.models.example_model import *
|
||||
from backend.models.user_model import *
|
||||
from backend.models.post_model import *
|
||||
from backend.models.recorder_model import *
|
||||
from backend.models.room_model import *
|
||||
from backend.models.virtual_command_model import *
|
||||
15
backend/models/example_model.py
Normal file
15
backend/models/example_model.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend import db
|
||||
import uuid
|
||||
|
||||
|
||||
class ExampleDataItem(db.Model):
|
||||
"""
|
||||
just an example class...
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
mac = db.Column(db.String(32), nullable=False, unique=True, index=True)
|
||||
uuid = db.Column(db.String(36), nullable=False, unique=True, index=True, default=str(uuid.uuid4()))
|
||||
some_string_value = db.Column(db.String, nullable=True, index=True)
|
||||
name = db.Column(db.String(128), default="<not set>", nullable=False, index=True, unique=False)
|
||||
description = db.Column(db.String(4096), nullable=True, unique=False)
|
||||
19
backend/models/post_model.py
Normal file
19
backend/models/post_model.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Example post model and related models
|
||||
"""
|
||||
|
||||
from backend import db
|
||||
|
||||
|
||||
class Post(db.Model):
|
||||
"""
|
||||
A post example class
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
body = db.Column(db.String(140))
|
||||
timestamp = db.Column(db.DateTime)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<Post %r>' % self.body
|
||||
143
backend/models/recorder_model.py
Normal file
143
backend/models/recorder_model.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Models for lecture recorder
|
||||
"""
|
||||
import json
|
||||
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import validates
|
||||
|
||||
from backend import db, app, login_manager
|
||||
import re
|
||||
from sqlalchemy import or_
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.models.virtual_command_model import virtual_command_recorder_command_table, virtual_command_recorder_table
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
|
||||
class RecorderModel(db.Model):
|
||||
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)
|
||||
_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_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_by_checksum(md5_sum):
|
||||
return RecorderModel.query.filter(RecorderModel.checksum == md5_sum).first()
|
||||
|
||||
@hybrid_property
|
||||
def requires_user(self):
|
||||
return self._requires_user > 0
|
||||
|
||||
@requires_user.setter
|
||||
def requires_user(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
|
||||
|
||||
|
||||
class Recorder(db.Model):
|
||||
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)
|
||||
locked = db.Column(db.Boolean, default=False)
|
||||
lock_message = db.Column(db.String, nullable=True, default=None)
|
||||
offline = db.Column(db.Boolean, default=False)
|
||||
description = db.Column(db.Unicode(255), unique=False, nullable=True, default="")
|
||||
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)
|
||||
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)
|
||||
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')
|
||||
|
||||
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_all():
|
||||
return Recorder.query.all()
|
||||
|
||||
@validates('name')
|
||||
def validate_name(self, key, value):
|
||||
assert len(value) > 2
|
||||
return value
|
||||
|
||||
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 RecorderCommand(db.Model):
|
||||
"""Table containing permissions associated with groups."""
|
||||
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)
|
||||
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')
|
||||
|
||||
@staticmethod
|
||||
def get_all():
|
||||
return RecorderCommand.query.all()
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
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)
|
||||
76
backend/models/room_model.py
Normal file
76
backend/models/room_model.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- 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
|
||||
from backend.tools.scrape_rooms import scrape_rooms
|
||||
|
||||
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)
|
||||
comment = db.Column(db.Unicode(2047), unique=False, nullable=True, default="")
|
||||
number = db.Column(db.Unicode(63), unique=False, nullable=True)
|
||||
building_name = db.Column(db.Unicode(63), unique=False, nullable=True)
|
||||
building_number = db.Column(db.Unicode(63), unique=False, nullable=True)
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
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_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():
|
||||
rooms = scrape_rooms()
|
||||
i_tunes_u_mappings = [Room(name=room['name'], number=room['room_number'],
|
||||
building_name=room['building_name'], building_number=room['building_number']) for room in
|
||||
rooms]
|
||||
|
||||
try:
|
||||
db.session.bulk_save_objects(i_tunes_u_mappings)
|
||||
db.session.commit()
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
485
backend/models/user_model.py
Normal file
485
backend/models/user_model.py
Normal file
@@ -0,0 +1,485 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Example user model and related models
|
||||
"""
|
||||
import json
|
||||
|
||||
from sqlalchemy.orm import relation
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
from backend import db, app, login_manager
|
||||
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'))
|
||||
)
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
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')
|
||||
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)
|
||||
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')
|
||||
|
||||
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
|
||||
|
||||
@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 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
|
||||
|
||||
@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))
|
||||
groups = db.relationship(Group, secondary=group_permission_table,
|
||||
back_populates='permissions')
|
||||
|
||||
|
||||
@event.listens_for(User.__table__, 'after_create')
|
||||
def insert_initial_users(*args, **kwargs):
|
||||
for u in app.config.get("USERS", []):
|
||||
db.session.add(User(**u))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@event.listens_for(Group.__table__, 'after_create')
|
||||
def insert_initial_groups(*args, **kwargs):
|
||||
for g in app.config.get("GROUPS", []):
|
||||
db.session.add(Group(**g))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@event.listens_for(Permission.__table__, 'after_create')
|
||||
def insert_initial_permissions(*args, **kwargs):
|
||||
for p in app.config.get("PERMISSIONS", []):
|
||||
db.session.add(Permission(name=p))
|
||||
db.session.commit()
|
||||
95
backend/models/virtual_command_model.py
Normal file
95
backend/models/virtual_command_model.py
Normal file
@@ -0,0 +1,95 @@
|
||||
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))
|
||||
|
||||
|
||||
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')
|
||||
|
||||
# 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_string = db.Column(db.String)
|
||||
|
||||
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_string is None:
|
||||
return []
|
||||
return json.loads(self.command_order_string)
|
||||
|
||||
@command_order.setter
|
||||
def command_order(self, ordered_list_of_commands: list):
|
||||
self.command_order_string = json.dumps(ordered_list_of_commands)
|
||||
|
||||
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)
|
||||
136
backend/recorder_adapters/__init__.py
Normal file
136
backend/recorder_adapters/__init__.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import inspect
|
||||
import pkgutil
|
||||
import sys
|
||||
import telnetlib
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# monkey patching of telnet lib
|
||||
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:
|
||||
@abstractmethod
|
||||
def _get_name(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_version(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_defined_recorder_adapters():
|
||||
models = []
|
||||
found_packages = list(pkgutil.iter_modules(sys.modules[__name__].__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': {}}
|
||||
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, RecorderAdapter):
|
||||
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
|
||||
786
backend/recorder_adapters/extron_smp.py
Normal file
786
backend/recorder_adapters/extron_smp.py
Normal file
@@ -0,0 +1,786 @@
|
||||
from backend.recorder_adapters import telnetlib, TelnetAdapter, RecorderAdapter
|
||||
|
||||
RECORDER_MODEL_NAME = "SMP 351 / 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
|
||||
|
||||
USER = "admin"
|
||||
PW = "123mzsmp"
|
||||
|
||||
|
||||
class SMP(TelnetAdapter, RecorderAdapter):
|
||||
def __init__(self, address, password, **kwargs):
|
||||
super().__init__(address)
|
||||
self.admin_pw = password
|
||||
|
||||
def _login(self):
|
||||
self.tn = telnetlib.Telnet(HOST)
|
||||
self.tn.read_until("\r\nPassword:")
|
||||
# password = getpass.getpass()
|
||||
password = self.admin_pw
|
||||
self.tn.write(password + "\n\r")
|
||||
|
||||
out = self.tn.assert_string_in_output("Login Administrator")
|
||||
if not out[0]:
|
||||
print(out[1])
|
||||
if "Password:" in out[1]:
|
||||
# TODO: loop until logged in...
|
||||
print("re-enter pw")
|
||||
self.tn.write("123mzsmp\n\r")
|
||||
print(self.tn.assert_string_in_output("Login Administrator"))
|
||||
print("WRONG (admin) password!! Exiting!")
|
||||
self.tn = None
|
||||
raise Exception("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 Exception("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):
|
||||
pass
|
||||
|
||||
def restore_configuration(self):
|
||||
pass
|
||||
"""
|
||||
|
||||
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 Exception("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 Exception("input_num must be a value between 1 and 5!")
|
||||
if channel_num not in range(1, 3):
|
||||
raise Exception("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 Exception("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 Exception("input_num must be a value between 1 and 5!")
|
||||
if input_format not in range(1, 4):
|
||||
raise Exception("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 Exception("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 Exception("input_num must be a value between 1 and 5!")
|
||||
if len(input_name) > 16:
|
||||
raise Exception("input_name must be no longer than 16 chars")
|
||||
try:
|
||||
input_name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise Exception("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 Exception("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_selction_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())
|
||||
|
||||
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 TelnetAdapter._get_response_str(self.tn.read_until_non_empty_line())
|
||||
|
||||
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 Exception("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 Exception("channel_number must be a value between 1 and 2!")
|
||||
if preset_number not in range(1, 17):
|
||||
raise Exception("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 Exception("channel_number must be a value between 1 and 2!")
|
||||
if preset_number not in range(1, 17):
|
||||
raise Exception("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 Exception("preset_number must be a value between 1 and 16!")
|
||||
if len(preset_name) > 16:
|
||||
raise Exception("preset_name must be no longer than 16 chars")
|
||||
try:
|
||||
preset_name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise Exception("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 Exception("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 Exception("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 Exception("channel_number must be a value between 1 and 2!")
|
||||
if preset_number not in range(1, 129):
|
||||
raise Exception("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 Exception("channel_number must be a value between 1 and 2!")
|
||||
if preset_number not in range(1, 129):
|
||||
raise Exception("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 Exception("preset_number must be a value between 1 and 128!")
|
||||
if len(preset_name) > 16:
|
||||
raise Exception("preset_name must be no longer than 16 chars")
|
||||
try:
|
||||
preset_name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise Exception("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 Exception("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 Exception("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 Exception("output_number must be a value between 1 and 3!")
|
||||
if preset_number not in range(1, 17):
|
||||
raise Exception("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 Exception("output_number must be a value between 1 and 3!")
|
||||
if preset_number not in range(1, 17):
|
||||
raise Exception("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 Exception("preset_number must be a value between 1 and 16!")
|
||||
if len(preset_name) > 16:
|
||||
raise Exception("preset_name must be no longer than 16 chars")
|
||||
try:
|
||||
preset_name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise Exception("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 Exception("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 Exception("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 Exception("output_number must be a value between 1 and 3!")
|
||||
if preset_number not in range(1, 17):
|
||||
raise Exception("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 Exception("output_number must be a value between 1 and 3!")
|
||||
if preset_number not in range(1, 17):
|
||||
raise Exception("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 Exception("preset_number must be a value between 1 and 16!")
|
||||
if len(preset_name) > 16:
|
||||
raise Exception("preset_name must be no longer than 16 chars")
|
||||
try:
|
||||
preset_name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise Exception("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 Exception("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 Exception("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 Exception("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 Exception("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 Exception("preset_number must be a value between 1 and 16!")
|
||||
if len(preset_name) > 16:
|
||||
raise Exception("preset_name must be no longer than 16 chars")
|
||||
try:
|
||||
preset_name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise Exception("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 Exception("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 Exception("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 Exception("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 Exception("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 Exception("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
|
||||
"""
|
||||
|
||||
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 Exception("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 Exception("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 Exception("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 Exception("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 = SMP(HOST, PW)
|
||||
print(smp)
|
||||
smp._login()
|
||||
print(smp.get_version(verbose_info=False))
|
||||
|
||||
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()
|
||||
|
||||
76
backend/serve_frontend.py
Normal file
76
backend/serve_frontend.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/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
|
||||
|
||||
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.")
|
||||
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()
|
||||
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
|
||||
|
||||
72
backend/tools/model_updater.py
Normal file
72
backend/tools/model_updater.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019. Tobias Kurze
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from json import dumps
|
||||
from pprint import pprint
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
from backend import db
|
||||
from backend.models.recorder_model import RecorderModel, RecorderCommand
|
||||
from backend.recorder_adapters import get_defined_recorder_adapters
|
||||
|
||||
|
||||
def calculate_md5_checksum(string_to_md5_sum: str):
|
||||
return hashlib.md5(string_to_md5_sum.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def create_recorder_commands_for_recorder_adapter(command_definitions: dict, recorder_model: RecorderModel):
|
||||
existing_recorder_commands = RecorderCommand.query.filter(
|
||||
and_(RecorderCommand.name.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):
|
||||
logging.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:
|
||||
# print(c_d)
|
||||
args = command_definitions.get(c_d)
|
||||
# create new recorder command(s)
|
||||
r_c = RecorderCommand(name=c_d, parameters=args, recorder_model=recorder_model)
|
||||
db.session.add(r_c)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def update_recorder_models_database():
|
||||
r_as = get_defined_recorder_adapters()
|
||||
for r_a in r_as:
|
||||
r_m = RecorderModel.get_by_adapter_id(r_a["id"])
|
||||
model_checksum = calculate_md5_checksum(dumps(r_a["commands"]))
|
||||
if r_m is None:
|
||||
r_m = RecorderModel(record_adapter_id=r_a["id"], model_name=r_a["name"], checksum=model_checksum,
|
||||
requires_user=r_a.get('requires_user', None),
|
||||
requires_password=r_a.get('requires_password', None))
|
||||
db.session.add(r_m)
|
||||
db.session.flush()
|
||||
db.session.refresh(r_m)
|
||||
else:
|
||||
if not model_checksum == r_m.checksum:
|
||||
r_m.model_name = r_a["name"]
|
||||
r_m.model_name = r_a["name"]
|
||||
r_m.checksum = model_checksum
|
||||
create_recorder_commands_for_recorder_adapter(r_a["commands"], r_m)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
db.create_all()
|
||||
update_recorder_models_database()
|
||||
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__':
|
||||
scrape_rooms()
|
||||
Reference in New Issue
Block a user