From 5d731c9fbafd6565899f4df0671448de1651280b Mon Sep 17 00:00:00 2001 From: tobias Date: Thu, 31 Oct 2019 16:13:02 +0100 Subject: [PATCH] moved some models added cron and websocket base class --- backend/api/models.py | 94 ++++++++++++++++++++++ backend/api/recorder_api.py | 69 +--------------- backend/api/user_api.py | 50 ++++++++---- backend/auth/oidc.py | 5 +- backend/cron/__init__.py | 7 ++ backend/models/recorder_model.py | 4 + backend/models/user_model.py | 14 ++++ backend/websocket/base.py | 59 ++++++++++++++ backend/websocket/websocket_base.py | 8 +- backend/websocket/websocket_test_client.py | 2 +- 10 files changed, 224 insertions(+), 88 deletions(-) create mode 100644 backend/api/models.py create mode 100644 backend/cron/__init__.py create mode 100644 backend/websocket/base.py diff --git a/backend/api/models.py b/backend/api/models.py new file mode 100644 index 0000000..e774d09 --- /dev/null +++ b/backend/api/models.py @@ -0,0 +1,94 @@ +from flask_restplus import fields +from backend.api import api_user, api_recorder, api_v1 + +generic_id_parser = api_v1.parser() +generic_id_parser.add_argument('id', type=str, required=True, store_missing=False) + +user_model = api_user.model('User', { + 'id': fields.String(required=True, description='The user\'s identifier'), + 'first_name': fields.String(required=True, description='The user\'s first name'), + 'last_name': fields.String(required=True, description='The user\'s last name'), + 'email': fields.String(required=True, description='The user\'s email address'), + 'nickname': fields.String(required=False, description='The user\'s nick name'), + 'last_seen': fields.DateTime(required=False, description='Last time user logged in'), + 'last_time_modified': fields.DateTime(required=False, description='Last time user was modified'), + 'role': fields.String(required=False, description='Role a user might have (in addition to group memberships)'), + 'effective_permissions': fields.List( + fields.String(required=True), required=False, description="List of permissions (groups + (optional) role)." + ), + 'groups': fields.List( + fields.Nested(api_user.model('user_group', {'id': fields.Integer(), 'name': fields.String()})), + required=False, description='Group memberships.'), + 'favorite_recorders': fields.List( + fields.Nested(api_user.model('favorite_recorder', {'id': fields.Integer(), 'name': fields.String()})), + required=False, description='Favorite recorders.'), +}) + +recorder_model = api_recorder.model('Recorder', { + 'id': fields.String(required=False, description='The recorder\'s identifier'), + 'created_at': fields.DateTime(required=False, description='Creation date of the recorder'), + 'last_time_modified': fields.DateTime(required=False, description='Creation date of the recorder'), + 'name': fields.String(min_length=3, required=True, description='The recorder\'s name'), + '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") +}) + diff --git a/backend/api/recorder_api.py b/backend/api/recorder_api.py index 5970c03..0457094 100644 --- a/backend/api/recorder_api.py +++ b/backend/api/recorder_api.py @@ -13,78 +13,11 @@ from flask_restplus import fields, Resource, inputs from backend import db, app from backend.api import api_recorder +from backend.api.models import recorder_model, recorder_model_model, recorder_command_model 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") -}) - # == diff --git a/backend/api/user_api.py b/backend/api/user_api.py index 4b933d0..af2834b 100644 --- a/backend/api/user_api.py +++ b/backend/api/user_api.py @@ -8,28 +8,15 @@ 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 flask_restplus import Resource, fields, inputs, abort from backend import db, app, jwt_auth from backend.api import api_user +from backend.api.models import user_model, recorder_model, generic_id_parser +from backend.models import Recorder 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'), - 'role': fields.String(required=False, description='Role a user might have (in addition to group memberships)'), - 'effective_permissions': fields.List( - fields.String(required=True), required=False, description="List of permissions (groups + (optional) role)." - ), - 'groups': fields.List( - fields.Nested(api_user.model('user_group', {'id': fields.Integer(), 'name': fields.String()})), - required=False, description='Group memberships.'), -}) user_update_parser = api_user.parser() user_update_parser.add_argument('email', type=inputs.email(), required=False, nullable=False, store_missing=False) @@ -64,6 +51,37 @@ class Profile(Resource): return "ok" +@api_user.route('/profile/favorite_recorders') +class UserFavoriteRecorders(Resource): + @jwt_required + @api_user.marshal_list_with(recorder_model) + def get(self): + try: + current_user_id = get_jwt_identity() + return User.get_by_identifier(current_user_id).favorite_recorders + except AttributeError: + abort(404, "User not found!") + + @jwt_required + @api_user.expect(generic_id_parser) + @api_user.marshal_list_with(recorder_model) + def put(self): + try: + args = generic_id_parser.parse_args() + current_user_id = get_jwt_identity() + user = User.get_by_identifier(current_user_id) + print(user) + recorder = Recorder.get_by_identifier(args["id"]) + print(recorder) + if recorder is None: + abort(404, "(Specified [id: {}]) recorder not found!".format(args["id"])) + user.favorite_recorders.append(recorder) + db.session.commit() + return user.favorite_recorders + except AttributeError: + abort(404, "User not found!") + + @api_user.route('') class UserList(Resource): """ diff --git a/backend/auth/oidc.py b/backend/auth/oidc.py index 77011e8..943cf70 100644 --- a/backend/auth/oidc.py +++ b/backend/auth/oidc.py @@ -2,6 +2,7 @@ """ OIDC login auth module """ +from datetime import datetime import flask from flask import jsonify, redirect, url_for @@ -45,7 +46,9 @@ def create_or_retrieve_user_from_userinfo(userinfo): if user is not None: app.logger.info("user found") - #TODO: update user! + user.last_seen = datetime.utcnow() + # TODO: update user! + db.session.commit() return user user = User(email=email, first_name=userinfo.get("given_name", ""), diff --git a/backend/cron/__init__.py b/backend/cron/__init__.py new file mode 100644 index 0000000..2609fec --- /dev/null +++ b/backend/cron/__init__.py @@ -0,0 +1,7 @@ +import logging + +cron_log_handler = logging.FileHandler(CRON_LOG_FILE) +cron_logger = logging.getLogger("mal.cron") +cron_logger.addHandler(cron_log_handler) +logging.getLogger("apscheduler.scheduler").addHandler(cron_log_handler) +logging.getLogger("apscheduler.executors.default").addHandler(cron_log_handler) \ No newline at end of file diff --git a/backend/models/recorder_model.py b/backend/models/recorder_model.py index 2e7f143..42a646e 100644 --- a/backend/models/recorder_model.py +++ b/backend/models/recorder_model.py @@ -93,6 +93,10 @@ class Recorder(db.Model): def get_by_name(name): return Recorder.query.filter(Recorder.name == name).first() + @staticmethod + def get_by_identifier(identifier): + return Recorder.query.filter(Recorder.id == identifier).first() + @staticmethod def get_all(): return Recorder.query.all() diff --git a/backend/models/user_model.py b/backend/models/user_model.py index 1d6e47f..0027c9b 100644 --- a/backend/models/user_model.py +++ b/backend/models/user_model.py @@ -31,6 +31,18 @@ acquaintances = db.Table('acquaintances', db.Column('acquaintance_id', db.Integer, db.ForeignKey('user.id')) ) +user_favorite_recorders_table = db.Table('user_favorite_recorders', + db.Column('user_id', db.Integer, + db.ForeignKey('user.id', + onupdate="CASCADE", + ondelete="CASCADE"), + primary_key=True), + db.Column('recorder_id', db.Integer, + db.ForeignKey('recorder.id', + onupdate="CASCADE", + ondelete="CASCADE"), + primary_key=True)) + # This is the association table for the many-to-many relationship between # groups and members - this is, the memberships. user_group_table = db.Table('user_group', @@ -97,6 +109,8 @@ class User(UserMixin, db.Model): backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') + favorite_recorders = db.relationship('Recorder', secondary=user_favorite_recorders_table) + def __init__(self, **kwargs): super(User, self).__init__(**kwargs) password = kwargs.get("password", None) diff --git a/backend/websocket/base.py b/backend/websocket/base.py new file mode 100644 index 0000000..53cdd5e --- /dev/null +++ b/backend/websocket/base.py @@ -0,0 +1,59 @@ +import logging +import threading + +from flask_login import current_user +from flask_socketio import SocketIO, emit + +from backend import app + +logger = logging.getLogger("lrc.websocket") + +async_mode = 'threading' # set to traditional python threading model +# async_mode = None +socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode) + + +class WebSocketBase: + def __init__(self, flask_app_context=None): + if flask_app_context is None: + self.flask_app_context = app + self.socket_thread = None + + def start_websocket_in_thread(self, host=None, port=None, debug=None): + self.socket_thread = threading.Thread( + target=self.start_websocket, + args=(host, port, debug)) + self.socket_thread.start() + return self.socket_thread + + def start_websocket(self, host=None, port=None, debug=None): + if debug is None: + debug = self.flask_app_context.debug + socketio.run(self.flask_app_context, host=host, port=port, debug=debug) + + @staticmethod + @socketio.on('connect') + def connect_handler(): + logger.debug("new connection...") + print(current_user) + if current_user.is_authenticated: + logger.debug("user is authenticated") + print("allowed!") + emit('my response', + {'message': '{0} has joined'.format(current_user.name)}, + broadcast=True) + else: + logger.info("user is not authenticated!") + print("not allowed!!") + return False # not allowed here + + @socketio.on_error() + def handle_error(self, error): + logger.error(error) + print(error) + + +if __name__ == '__main__': + wsb = WebSocketBase() + #wsb.start_websocket_in_thread(debug=True) + wsb.start_websocket(debug=True) diff --git a/backend/websocket/websocket_base.py b/backend/websocket/websocket_base.py index 56aac30..bbf24e2 100644 --- a/backend/websocket/websocket_base.py +++ b/backend/websocket/websocket_base.py @@ -115,9 +115,13 @@ if __name__ == '__main__': # socketio.emit('server_event', "You!: server_event", namespace="/") # print("send bla") - socketio.run(app, host="localhost", port=5000, debug=True) + + #socketio.run(app, host="localhost", port=5000, debug=True) + start_in_thread() + while True: + time.sleep(2) + print("still running! :)") # socketio.run(app, debug=True) # ENDE - print("running?!") # t.join() diff --git a/backend/websocket/websocket_test_client.py b/backend/websocket/websocket_test_client.py index 9e8e70f..ea62a9c 100644 --- a/backend/websocket/websocket_test_client.py +++ b/backend/websocket/websocket_test_client.py @@ -15,7 +15,7 @@ app.config['SECRET_KEY'] = 'secret!' logging.basicConfig() #socketio = SocketIO(message_queue="redis://") -socketio = SocketIO(app) +socketio = SocketIO(app, port=5443, debug=True) #socketio.run(app, host="localhost", port=5000) #socketio.init_app(app, host="localhost", port=5000, cors_allowed_origins="*", )