diff --git a/backend/14/app.py b/backend/14/app.py new file mode 100644 index 0000000000000000000000000000000000000000..7456e94ce51fee4a4cc3091ae0a3f6ac8721b600 --- /dev/null +++ b/backend/14/app.py @@ -0,0 +1,623 @@ +import os +import shutil +from base64 import b64decode +from datetime import datetime +from subprocess import Popen, PIPE + +from flask import request, jsonify, json +from flask_bcrypt import Bcrypt +from flask_jwt_extended import JWTManager +from flask_jwt_extended import ( + create_access_token, + create_refresh_token, + jwt_required, + jwt_refresh_token_required, + get_jwt_identity +) +from simpleflock import SimpleFlock + +import database_helper as db +from __init__ import app +from socketio_helper import socketio + +WORKING_DIR = os.path.dirname(os.path.abspath(__file__)) + '/' + +# Http status codes +OK_STATUS_CODE = 200 +BAD_REQUEST_STATUS_CODE = 400 +CONFLICT_STATUS_CODE = 409 +UNAUTHORIZED_STATUS_CODE = 401 + +DEFAULT_EDITOR_SETTINGS = {'fontSize': 16, 'margin': 80, 'tabSize': 4} + +flask_bcrypt = Bcrypt(app) +jwt = JWTManager(app) + + +# GENERAL +def generate_response(response, status, headers={}): + response.status_code = status + response.headers = {**{ + 'Content-Type': 'application/json', + 'Access-Control-Expose-Headers': 'X-Auth-Token', + 'Access-Control-Allow-Origin': '*' + }, **headers} + return response + + +def update_project_edited(project_id): + project = db.get_project(project_id) + project.edited = datetime.now() + db.update_project(project) + + +# Authentication +def is_valid_credentials(username_email, password): + if '@' in username_email: + user = db.get_user_from_email(username_email) + else: + user = db.get_user_from_username(username_email) + return user and flask_bcrypt.check_password_hash(user.password, + b64decode(password).decode("utf-8")) + + +def is_valid_password(password): + return 8 <= len(password) <= 100 + + +def is_valid_collaborators(collaborators): + if not collaborators: + return True + accepted_permissions = ['View-only', 'May edit'] + for collaborator in collaborators: + if (not collaborator or not db.is_user_id(collaborator['userId']) or + not collaborator['permission'] in accepted_permissions): + return False + return True + + +@jwt.unauthorized_loader +def unauthorized_response(callback): + return jsonify({ + 'ok': False, + 'message': 'Missing Authorization Header' + }), 401 + + +@app.route('/api/auth', methods=["GET"]) +@jwt_required +def auth(): + return generate_response(jsonify({ + 'success': True, + 'message': 'Authorization successful' + }), OK_STATUS_CODE) + + +@app.route('/api/refresh', methods=['POST']) +@jwt_refresh_token_required +def refresh(): + current_user = get_jwt_identity() + return generate_response(jsonify({ + 'success': True, + 'message': 'Successfully refreshed token', + 'jwt': create_access_token(identity=current_user, fresh=False) + }), OK_STATUS_CODE) + + +# USER +@app.route('/api/sign_up', methods=['POST']) +def sign_up(): + email = request.json['email'] + username = request.json['username'] + password = b64decode(request.json['password']).decode("utf-8") + if not email or not username or not password: + return generate_response(jsonify({ + 'success': False, + 'message': 'Bad request.' + }), BAD_REQUEST_STATUS_CODE) + elif not db.is_unregistered_email(email.lower()): + return generate_response(jsonify({ + 'success': False, + 'message': 'User with that email already exists' + }), CONFLICT_STATUS_CODE) + elif not db.is_unregistered_username(username.lower()): + return generate_response(jsonify({ + 'success': False, + 'message': 'User with that username already exists' + }), CONFLICT_STATUS_CODE) + elif not is_valid_password(password): + return generate_response(jsonify({ + 'success': False, + 'message': 'Password does not fulfill requirements' + }), BAD_REQUEST_STATUS_CODE) + else: + new_user = db.User( + email.lower(), + username.lower(), + flask_bcrypt.generate_password_hash(password).decode('utf-8')) + db.add_user(new_user) + + new_editor_settings = db.EditorSettings( + DEFAULT_EDITOR_SETTINGS['fontSize'], + DEFAULT_EDITOR_SETTINGS['margin'], + DEFAULT_EDITOR_SETTINGS['tabSize']) + db.add_editor_settings(new_editor_settings) + + new_user.editorSettingsId = new_editor_settings.id + db.update_user(new_user.id, new_user) + return generate_response(db.user_schema.jsonify(new_user), + OK_STATUS_CODE) + + +@app.route('/api/sign_in', methods=['GET']) +def sign_in(): + auth_data = request.authorization + username_email = auth_data.username + password = auth_data.password + + if not username_email or not password: + message = 'Missing credentials!' + status = BAD_REQUEST_STATUS_CODE + elif not is_valid_credentials(username_email.lower(), password): + message = 'Incorrect username/email or password' + status = UNAUTHORIZED_STATUS_CODE + else: + if '@' in username_email: + username = db.get_username_from_email(username_email.lower()) + else: + username = username_email + + db_user = db.get_user_from_username(username.lower()) + user = { + 'id': db_user.id, + 'email': db_user.email, + 'username': db_user.username, + 'editorSettingsId': db_user.editorSettingsId, + 'editorSettings': db.editor_settings_schema.dumps(db_user.editorSettings), + 'jwt': create_access_token(identity=username), + 'refreshToken': create_refresh_token(identity=username) + } + return generate_response(jsonify(user), OK_STATUS_CODE) + + return generate_response(jsonify({ + 'success': False, + 'message': message + }), status) + + +@app.route('/api/change_password', methods=['POST', 'PUT']) +@jwt_required +def change_password(): + username = request.json['username'] + user = db.get_user_from_username(username) + old_password = request.json['oldPassword'] + new_password = request.json['newPassword'] + if not user: + return generate_response(jsonify({ + 'success': False, + 'message': 'No user with that username' + }), BAD_REQUEST_STATUS_CODE) + elif not is_valid_credentials(username, old_password): + return generate_response(jsonify({ + 'success': False, + 'message': 'Old password was incorrect' + }), UNAUTHORIZED_STATUS_CODE) + elif not is_valid_password(new_password): + return generate_response(jsonify({ + 'success': False, + 'message': 'New password does not fulfill requirements' + }), BAD_REQUEST_STATUS_CODE) + else: + updated_user = db.change_password( + username, + flask_bcrypt.generate_password_hash(new_password).decode('utf-8') + ) + return generate_response(db.user_schema.jsonify(updated_user), OK_STATUS_CODE) + + +@app.route('/api/delete_account', methods=['DELETE']) +def delete_user(): + auth_data = request.authorization + username = auth_data.username + password = auth_data.password + user = db.get_user_from_username(username) + if not is_valid_credentials(username, password): + return generate_response(jsonify({ + 'success': False, + 'message': 'Password was incorrect' + }), UNAUTHORIZED_STATUS_CODE) + elif not user: + return generate_response(jsonify({ + 'success': False, + 'message': 'No user with that username' + }), BAD_REQUEST_STATUS_CODE) + else: + db.delete_user_projects(user.id) + db.delete_user_collaborators(user.id) + db.delete_editor_settings(user.editorSettingsId) + db.delete_user(user.id) + return generate_response(jsonify({ + 'success': True, + 'message': 'Successfully deleted account' + }), OK_STATUS_CODE) + + +@app.route('/api/get_users', methods=['GET']) +@jwt_required +def get_users_request(): + return generate_response(db.users_schema.jsonify(db.get_users()), + OK_STATUS_CODE) + + +@app.route('/api/get_user/<user_id>', methods=['GET']) +@jwt_required +def get_user_request(user_id): + return generate_response(db.user_schema.jsonify(db.get_user(user_id)), + OK_STATUS_CODE) + + +# EDITOR SETTINGS +@app.route('/api/update_editor_settings/<user_id>', methods=['PUT']) +@jwt_required +def update_editor_settings_request(user_id): + editor_settings = request.json['editorSettings'] + updated_editor_settings = db.EditorSettings( + editor_settings['fontSize'], + editor_settings['margin'], + editor_settings['tabSize'], + ) + updated_editor_settings.id = editor_settings['id'] + return generate_response(db.editor_settings_schema.jsonify( + db.update_editor_settings(updated_editor_settings)), OK_STATUS_CODE) + + +# PROJECT +@app.route('/api/add_project', methods=['POST']) +@jwt_required +def new_project_request(): + title = request.json['title'] + creator_id = request.json['creatorId'] + collaborators = request.json['collaborators'] if request.json['collaborators'] != [] else None + if not title or not creator_id: + return generate_response(jsonify({ + 'success': False, + 'message': 'Bad request.' + }), BAD_REQUEST_STATUS_CODE) + elif not db.is_user_id(creator_id): + return generate_response(jsonify({ + 'success': False, + 'message': 'User does not exist' + }), BAD_REQUEST_STATUS_CODE) + elif not is_valid_collaborators(collaborators): + return generate_response(jsonify({ + 'success': False, + 'message': 'Invalid collaborators' + }), BAD_REQUEST_STATUS_CODE) + else: + new_project = db.Project(title, creator_id) + db.add_project(new_project) + + if collaborators: + for collaborator in collaborators: + new_collaborator = db.Collaborator( + project_id=new_project.id, + user_id=collaborator['userId'], + permission=collaborator['permission']) + db.add_collaborator(new_collaborator) + + return generate_response(db.project_schema.jsonify(new_project), OK_STATUS_CODE) + + +@app.route('/api/duplicate_project/<project_id>', methods=['POST']) +@jwt_required +def duplicate_project_request(project_id): + title = request.json['title'] + creator_id = request.json['creatorId'] + if not title or not creator_id: + return generate_response(jsonify({ + 'success': False, + 'message': 'Bad request.' + }), BAD_REQUEST_STATUS_CODE) + elif not db.is_user_id(creator_id): + return generate_response(jsonify({ + 'success': False, + 'message': 'User does not exist' + }), BAD_REQUEST_STATUS_CODE) + else: + old_project = db.get_project(project_id) + duplicated_project = db.Project( + title, + creator_id, + archived=old_project.archived + ) + db.add_project(duplicated_project) + + collaborators = db.get_project_collaborators(project_id) + if collaborators: + for collaborator in collaborators: + if collaborator['userId'] != creator_id: + duplicated_collaborator = db.Collaborator( + project_id=duplicated_project.id, + user_id=collaborator['userId'], + permission=collaborator['permission']) + db.add_collaborator(duplicated_collaborator) + + if old_project.creatorId != creator_id: + old_creator_collaborator = db.Collaborator( + project_id=duplicated_project.id, + user_id=old_project.creatorId, + permission='May edit') + db.add_collaborator(old_creator_collaborator) + + files = db.get_project_files(project_id) + if files: + for file in files: + duplicated_file = db.File( + project_id=duplicated_project.id, + name=file['name'], + is_folder=file['isFolder'], + parent=file['parent'], + content=file['content']) + db.add_file(duplicated_file) + + return generate_response(db.project_schema.jsonify(duplicated_project), OK_STATUS_CODE) + + +@app.route('/api/get_projects', methods=['GET']) +@jwt_required +def get_projects_request(): + return generate_response(db.projects_schema.jsonify(db.get_projects()), OK_STATUS_CODE) + + +@app.route('/api/get_projects/<user_id>', methods=['GET']) +@jwt_required +def get_user_projects_request(user_id): + return generate_response(db.projects_schema.jsonify(db.get_user_projects(user_id)), + OK_STATUS_CODE) + + +@app.route('/api/get_project/<project_id>', methods=['GET']) +@jwt_required +def get_project_request(project_id): + return generate_response(db.project_schema.jsonify(db.get_project(project_id)), + OK_STATUS_CODE) + + +@app.route('/api/update_project/<project_id>', methods=['POST', 'PUT']) +@jwt_required +def update_project_request(project_id): + project = json.loads(request.json['project']) + updated_project = db.Project( + title=project['title'], + creator_id=project['creatorId'], + archived=project['archived']) + updated_project.id = project['id'] + + return generate_response(db.project_schema.jsonify( + db.update_project(updated_project)), OK_STATUS_CODE) + + +@app.route('/api/set_archived_project/<project_id>', methods=['POST', 'PUT']) +@jwt_required +def set_archived_project_request(project_id): + project = db.get_project(project_id) + project.archived = request.json['archived'] + + return generate_response(db.project_schema.jsonify( + db.update_project(project)), OK_STATUS_CODE) + + +@app.route('/api/delete_project/<project_id>', methods=['DELETE']) +@jwt_required +def delete_project_request(project_id): + db.delete_project_files(project_id) + db.delete_project_collaborators(project_id) + db.delete_project_messages(project_id) + db.delete_project(project_id) + return generate_response(jsonify({ + 'success': True, + 'message': 'Successfully deleted project' + }), OK_STATUS_CODE) + + +@app.route('/api/parse_project_files/<project_id>', methods=['GET']) +@jwt_required +def parse_file_request(project_id): + if db.get_project(project_id): + if os.path.exists(str(project_id)): + with SimpleFlock(str(project_id) + 'lock'): + find_cmd = 'find ' + str(project_id) + '/ -type f -name "*.py"' + find = Popen(find_cmd, shell=True, stdout=PIPE) + files = find.communicate()[0].decode('utf-8') + files = ' '.join(files.splitlines()) + + if files: + pylint_cmd = 'pylint --output-format=json ' + files + pylint = Popen(pylint_cmd, shell=True, stdout=PIPE) + result = pylint.communicate()[0].decode('utf-8') + else: + result = '[]' + else: + result = '[]' + return generate_response(jsonify({ + 'success': True, + 'message': 'Successfully parsed files.', + 'result': result + }), OK_STATUS_CODE) + else: + return generate_response(jsonify({ + 'success': False, + 'message': 'No such project' + }), BAD_REQUEST_STATUS_CODE) + + +# FILE +@app.route('/api/create_file', methods=['POST']) +@jwt_required +def create_file_request(): + file = request.json['file'] + new_file = db.File( + file['projectId'], + file['name'], + file['isFolder'], + file['parent'], + file['content'], + ) + db.add_file(new_file) + + update_project_edited(new_file.projectId) + + # Add file locally on server if project is open + open_project_path = str(new_file.projectId) + '/' + if os.path.exists(open_project_path): + if new_file.parent: + file_path = open_project_path + new_file.parent + '/' + new_file.name + else: + file_path = open_project_path + new_file.name + + if new_file.isFolder: + os.makedirs(file_path) + else: + f = open(file_path, 'w+') + f.write(new_file.content) + f.close() + + app.logger.error(new_file) + return generate_response(db.file_schema.jsonify( + new_file), OK_STATUS_CODE) + + +@app.route('/api/update_file/<file_id>', methods=['PUT']) +@jwt_required +def update_file_request(file_id): + file = request.json['file'] + old_file = db.get_file(file['id']) + + updated_file = db.File( + file['projectId'], + file['name'], + file['isFolder'], + file['parent'], + file['content']) + updated_file.id = file['id'] + + update_project_edited(updated_file.projectId) + project = db.get_project(updated_file.projectId) + + # Update file locally on server if project is open + open_project_path = str(project.id) + '/' + if old_file.parent: + old_path = open_project_path + old_file.parent + '/' + old_file.name + else: + old_path = open_project_path + old_file.name + if os.path.exists(old_path): + with SimpleFlock(str(project.id) + 'lock'): + if os.path.exists(old_path): + if file['parent']: + new_path = open_project_path + file['parent'] + '/' + file['name'] + else: + new_path = open_project_path + file['name'] + os.rename(WORKING_DIR + old_path, WORKING_DIR + new_path) + + return generate_response(db.file_schema.jsonify( + db.update_file(updated_file)), OK_STATUS_CODE) + + +@app.route('/api/delete_file/<file_id>', methods=['DELETE']) +@jwt_required +def delete_file_request(file_id): + file = db.get_file(file_id) + + update_project_edited(file.projectId) + + # Remove file locally on server if project is open + open_project_path = str(file.projectId) + '/' + if file.parent: + path = open_project_path + file.parent + '/' + file.name + else: + path = open_project_path + file.name + if os.path.exists(path): + with SimpleFlock(str(file.projectId) + 'lock'): + if os.path.exists(path): + if file.isFolder: + shutil.rmtree(path) + else: + os.remove(path) + + return generate_response(db.file_schema.jsonify( + db.delete_file(file_id)), OK_STATUS_CODE) + + +@app.route('/api/get_project_files/<project_id>', methods=['GET']) +@jwt_required +def get_project_files_request(project_id): + return generate_response(db.files_schema.jsonify( + db.get_project_files(project_id)), OK_STATUS_CODE) + + +# COLLABORATOR +@app.route('/api/add_collaborator', methods=['POST']) +@jwt_required +def add_collaborator_request(): + collaborator = request.json['collaborator'] + new_collaborator = db.Collaborator( + collaborator['projectId'], + collaborator['userId'], + collaborator['permission'], + ) + db.add_collaborator(new_collaborator) + + update_project_edited(new_collaborator.projectId) + + return generate_response(db.collaborator_schema.jsonify( + new_collaborator), OK_STATUS_CODE) + + +@app.route('/api/update_collaborator/<collaborator_id>', methods=['PUT']) +@jwt_required +def update_collaborator_request(collaborator_id): + # Update collaborator + collaborator = request.json['collaborator'] + updated_collaborator = db.Collaborator( + collaborator['projectId'], + collaborator['userId'], + collaborator['permission'] + ) + updated_collaborator.id = collaborator['id'] + + update_project_edited(updated_collaborator.projectId) + + return generate_response(db.collaborator_schema.jsonify( + db.update_collaborator(updated_collaborator)), OK_STATUS_CODE) + + +@app.route('/api/delete_collaborator/<collaborator_id>', methods=['DELETE']) +@jwt_required +def delete_collaborator_request(collaborator_id): + collaborator = db.get_collaborator(collaborator_id) + update_project_edited(collaborator.projectId) + + return generate_response(db.collaborator_schema.jsonify( + db.delete_collaborator(collaborator_id)), OK_STATUS_CODE) + + +@app.route('/api/get_project_collaborators/<project_id>', methods=['GET']) +@jwt_required +def get_project_collaborators_request(project_id): + return generate_response(db.collaborators_schema.jsonify( + db.get_project_collaborators(project_id)), OK_STATUS_CODE) + + +# MESSAGE +@app.route('/api/get_project_messages/<project_id>', methods=['GET']) +@jwt_required +def get_project_messages_request(project_id): + return generate_response(db.messages_schema.jsonify( + db.get_project_messages(project_id)), OK_STATUS_CODE) + + +if __name__ == '__main__': + socketio.run(app, host='0.0.0.0', port=6000) + + """ If you deleted the db, run the following to re-initialize it. """ + # db.db.create_all() diff --git a/backend/14/database_helper.py b/backend/14/database_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..c6a01ae774d5da70f6fcc0cd73556f1132858bb0 --- /dev/null +++ b/backend/14/database_helper.py @@ -0,0 +1,517 @@ +import os +from datetime import datetime + +from flask_marshmallow import Marshmallow +from flask_sqlalchemy import SQLAlchemy + +from __init__ import app + +basedir = os.path.abspath(os.path.dirname(__file__)) + +# Database +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'db.sqlite') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) +ma = Marshmallow(app) + + +# EditorSettings +class EditorSettings(db.Model): + id = db.Column(db.Integer, primary_key=True) + fontSize = db.Column(db.Integer, nullable=False) + margin = db.Column(db.Integer, nullable=False) + tabSize = db.Column(db.Integer, nullable=False) + + def __init__(self, font_size, margin, tab_size): + self.fontSize = font_size + self.margin = margin + self.tabSize = tab_size + + +class EditorSettingsSchema(ma.Schema): + class Meta: + model = EditorSettings + fields = ('id', 'fontSize', 'margin', 'tabSize') + + +editor_settings_schema = EditorSettingsSchema(strict=True) + + +# User +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(100), unique=True, nullable=False) + username = db.Column(db.String(50), unique=True, nullable=False) + password = db.Column(db.Text, nullable=False) + editorSettingsId = db.Column(db.Integer, db.ForeignKey(EditorSettings.id)) + editorSettings = db.relationship("EditorSettings") + + def __init__(self, email, username, password): + self.email = email + self.username = username + self.password = password + + +class UserSchema(ma.Schema): + editorSettings = ma.Nested(EditorSettingsSchema) + + class Meta: + model = User + fields = ('id', 'email', 'username', 'editorSettingsId', 'editorSettings') + + +user_schema = UserSchema(strict=True) +users_schema = UserSchema(many=True, strict=True) + + +# Project +class Project(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(50), nullable=False) + creatorId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) + creator = db.relationship('User') + created = db.Column(db.String(50), default=datetime.now, nullable=False) + edited = db.Column(db.String(50), onupdate=datetime.now) + archived = db.Column(db.Boolean, default=False, nullable=False) + + def __init__(self, title, creator_id, created=None, edited=None, archived=False): + self.title = title + self.creatorId = creator_id + self.created = created + self.edited = edited + self.archived = archived + + +class ProjectSchema(ma.Schema): + creator = ma.Nested(UserSchema) + + class Meta: + model = Project + fields = ('id', 'title', 'creatorId', 'creator', 'created', 'edited', 'archived') + + +project_schema = ProjectSchema(strict=True) +projects_schema = ProjectSchema(many=True, strict=True) + + +# File +class File(db.Model): + id = db.Column(db.Integer, primary_key=True) + projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False) + project = db.relationship('Project', lazy='subquery') + name = db.Column(db.String(100), nullable=False) + isFolder = db.Column(db.Boolean, nullable=False) + parent = db.Column(db.String(100)) + content = db.Column(db.Text) + + def __init__(self, project_id, name, is_folder, parent, content): + self.projectId = project_id + self.name = name + self.isFolder = is_folder + self.parent = parent + self.content = content + + +class FileSchema(ma.Schema): + project = ma.Nested(ProjectSchema) + + class Meta: + model = File + fields = ('id', 'projectId', 'project', 'name', 'isFolder', 'parent', 'content') + + +file_schema = FileSchema(strict=True) +files_schema = FileSchema(many=True, strict=True) + + +# Collaborator +class Collaborator(db.Model): + id = db.Column(db.Integer, primary_key=True) + projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False) + project = db.relationship('Project', lazy='subquery') + userId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) + user = db.relationship('User', lazy='subquery') + permission = db.Column(db.Text, nullable=False) + + def __init__(self, project_id, user_id, permission): + self.projectId = project_id + self.userId = user_id + self.permission = permission + + +class CollaboratorSchema(ma.Schema): + project = ma.Nested(ProjectSchema) + user = ma.Nested(UserSchema) + + class Meta: + fields = ('id', 'projectId', 'project', 'userId', 'user', 'permission') + + +collaborator_schema = CollaboratorSchema(strict=True) +collaborators_schema = CollaboratorSchema(many=True, strict=True) + + +# Message +class Message(db.Model): + id = db.Column(db.Integer, primary_key=True) + projectId = db.Column(db.Integer, db.ForeignKey(Project.id), nullable=False) + project = db.relationship('Project') + authorId = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) + author = db.relationship('User') + message = db.Column(db.Text) + time = db.Column(db.String(50), default=datetime.now, nullable=False) + + def __init__(self, project_id, author_id, message, time=None): + self.projectId = project_id + self.authorId = author_id + self.message = message + self.time = time + + +class MessageSchema(ma.Schema): + project = ma.Nested(ProjectSchema) + author = ma.Nested(UserSchema) + + class Meta: + fields = ('id', 'projectId', 'project', 'authorId', 'author', 'message', 'time') + + +message_schema = MessageSchema(strict=True) +messages_schema = MessageSchema(many=True, strict=True) + + +# USER +def add_user(new_user): + db.session.add(new_user) + db.session.commit() + + +def get_users(): + all_users = User.query.all() + result = users_schema.dump(all_users) + return result.data + + +def get_user(id): + user = User.query.get(id) + return user + + +def update_user(id, updated_user): + user = User.query.get(id) + user.email = updated_user.email + user.username = updated_user.username + user.password = updated_user.password + user.editorSettingsId = updated_user.editorSettingsId + db.session.commit() + return user + + +def delete_user(id): + user = User.query.get(id) + db.session.delete(user) + db.session.commit() + return user + + +def delete_all_users(): + num_users_deleted = db.session.query(User).delete() + db.session.commit() + return num_users_deleted + + +def is_user(user): + return user and 'id' in user.keys() and bool(User.query.get(user['id'])) + + +def is_user_id(user_id): + return user_id and bool(User.query.get(user_id)) + + +def is_unregistered_email(email): + return not bool(get_user_from_email(email)) + + +def is_unregistered_username(username): + return not bool(get_user_from_username(username)) + + +def get_user_from_email(email): + return User.query.filter_by(email=email).first() + + +def get_user_from_username(username): + return User.query.filter_by(username=username).first() + + +def get_username_from_email(email): + return get_user_from_email(email).username + + +def is_valid_email_password(email, password): + user = get_user_from_email(email) + valid_password = user.password if user else None + return password and password == valid_password + + +def is_valid_username_password(username, password): + user = get_user_from_username(username) + valid_password = user.password if user else None + return password and password == valid_password + + +def change_password(username, newPassword): + user = get_user_from_username(username) + user.password = newPassword + db.session.commit() + return user + + +# EDITOR SETTINGS +def add_editor_settings(editor_settings): + db.session.add(editor_settings) + db.session.commit() + + +def get_editor_settings(user_id): + return EditorSettings.query.filter_by(userId=user_id).first() + + +def update_editor_settings(updated_editor_settings): + editor_settings = EditorSettings.query.get(updated_editor_settings.id) + editor_settings.fontSize = updated_editor_settings.fontSize + editor_settings.margin = updated_editor_settings.margin + editor_settings.tabSize = updated_editor_settings.tabSize + db.session.commit() + return editor_settings + + +def delete_editor_settings(editor_settings_id): + editor_settings = EditorSettings.query.get(editor_settings_id) + db.session.delete(editor_settings) + db.session.commit() + return editor_settings + + +def delete_all_editor_settings(): + num_editor_settings_deleted = db.session.query(EditorSettings).delete() + db.session.commit() + return num_editor_settings_deleted + + +# PROJECT +def add_project(project): + db.session.add(project) + db.session.commit() + + +def get_projects(): + all_projects = Project.query.all() + result = projects_schema.dump(all_projects) + return result.data + + +def get_user_projects(user_id): + """ Returns all projects where user is the creator or a collaborator """ + user_projects = Project.query.filter_by(creatorId=user_id).all() + collaborator_project_ids = [p_id for p_id, in db.session.query(Collaborator.projectId).filter_by(userId=user_id)] + user_projects += Project.query.filter(Project.id.in_(collaborator_project_ids)).all() + result = projects_schema.dump(user_projects) + return result.data + + +def get_project(project_id): + project = Project.query.get(project_id) + return project + + +def update_project(updated_project): + project = Project.query.get(updated_project.id) + project.title = updated_project.title + project.creatorId = updated_project.creatorId + project.archived = updated_project.archived + db.session.commit() + return project + + +def delete_project(project_id): + project = Project.query.get(project_id) + db.session.delete(project) + db.session.commit() + return project + + +def delete_user_projects(user_id): + """ Deletes all projects owned by user with id. """ + user_projects_query = Project.query.filter_by(creatorId=user_id) + + user_projects = user_projects_query.all() + for project in user_projects: + delete_project_files(project.id) + delete_project_collaborators(project.id) + delete_project_messages(project.id) + delete_project(project.id) + + num_projects_deleted = user_projects_query.delete() + db.session.commit() + return num_projects_deleted + + +def delete_all_projects(): + num_projects_deleted = db.session.query(Project).delete() + db.session.commit() + return num_projects_deleted + + +# FILE +def add_file(file): + db.session.add(file) + db.session.commit() + + +def get_file(id): + return File.query.get(id) + + +def get_files(): + all_files = File.query.all() + result = files_schema.dump(all_files) + return result.data + + +def get_project_files(project_id): + project_files = File.query.filter_by(projectId=project_id).all() + result = files_schema.dump(project_files) + return result.data + + +def update_file(updated_file): + file = File.query.get(updated_file.id) + file.projectId = updated_file.projectId + file.name = updated_file.name + file.isFolder = updated_file.isFolder + file.parent = updated_file.parent + file.content = updated_file.content + db.session.commit() + return file + + +def delete_file(file_id): + file = File.query.filter_by(id=file_id).first() + db.session.delete(file) + db.session.commit() + return file + + +def delete_project_files(project_id): + num_project_files_deleted = File.query.filter_by(projectId=project_id).delete() + db.session.commit() + return num_project_files_deleted + + +def delete_all_files(): + num_files_deleted = db.session.query(File).delete() + db.session.commit() + return num_files_deleted + + +# COLLABORATOR +def add_collaborator(collaborator): + db.session.add(collaborator) + db.session.commit() + + +def get_collaborator(id): + collaborator = Collaborator.query.get(id) + return collaborator + + +def get_collaborators(): + all_collaborators = Collaborator.query.all() + result = collaborators_schema.dump(all_collaborators) + return result.data + + +def get_project_collaborators(project_id): + project_collaborators = Collaborator.query.filter_by(projectId=project_id).all() + result = collaborators_schema.dump(project_collaborators) + return result.data + + +def update_collaborator(updated_collaborator): + collaborator = Collaborator.query.get(updated_collaborator.id) + collaborator.projectId = updated_collaborator.projectId + collaborator.userId = updated_collaborator.userId + collaborator.permission = updated_collaborator.permission + db.session.commit() + return collaborator + + +def delete_collaborator(collaborator_id): + collaborator = Collaborator.query.filter_by(id=collaborator_id).first() + db.session.delete(collaborator) + db.session.commit() + return collaborator + + +def delete_user_collaborators(user_id): + num_collaborators_deleted = Collaborator.query.filter_by(userId=user_id).delete() + db.session.commit() + return num_collaborators_deleted + + +def delete_project_collaborators(project_id): + num_collaborators_deleted = Collaborator.query.filter_by(projectId=project_id).delete() + db.session.commit() + return num_collaborators_deleted + + +def delete_all_collaborators(): + num_collaborators_deleted = db.session.query(Collaborator).delete() + db.session.commit() + return num_collaborators_deleted + + +# Message +def add_message(message): + db.session.add(message) + db.session.commit() + + +def get_message(id): + message = Message.query.get(id) + return message + + +def get_messages(): + all_messages = Message.query.all() + result = messages_schema.dump(all_messages) + return result.data + + +def get_project_messages(project_id): + project_messages = Message.query.filter_by(projectId=project_id).all() + result = messages_schema.dump(project_messages) + return result.data + + +def update_message(updated_message): + message = Message.query.get(updated_message.id) + message.projectId = updated_message.projectId + message.authorId = updated_message.authorId + message.time = updated_message.time + db.session.commit() + return message + + +def delete_project_messages(project_id): + num_project_messages_deleted = Message.query.filter_by(projectId=project_id).delete() + db.session.commit() + return num_project_messages_deleted + + +def delete_all_messages(): + num_messages_deleted = db.session.query(Message).delete() + db.session.commit() + return num_messages_deleted diff --git a/backend/14/socketio_helper.py b/backend/14/socketio_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..0c5e2db51efacf5809bb103498eec8d9fc3875fc --- /dev/null +++ b/backend/14/socketio_helper.py @@ -0,0 +1,496 @@ +import math +import os +import shutil +import signal +import subprocess +from datetime import datetime +from subprocess import Popen, PIPE + +import diff_match_patch as dmp_module +from flask import json +from flask_socketio import SocketIO, emit, join_room, leave_room +from simpleflock import SimpleFlock + +import database_helper as db +from __init__ import app + +TERMINATED_NORMALLY = 0 +TERMINATED_ERROR = 1 +TERMINATED_CANCEL = 2 + +os.environ['PYTHONUNBUFFERED'] = '1' +socketio = SocketIO(app) +dmp = dmp_module.diff_match_patch() + +project_viewers = dict() + + +def has_viewers(project_id): + return project_viewers[str(project_id)] + + +def has_viewer(username, project_id): + return bool(get_viewer(username, project_id)) + + +def get_viewer(username, project_id): + for viewer in project_viewers[str(project_id)]: + if viewer['username'] == username: + return viewer + return None + + +def set_viewer(viewer, project_id): + filtered_viewers = [] + for curr_viewer in project_viewers[str(project_id)]: + if curr_viewer['username'] != viewer['username']: + filtered_viewers.append(curr_viewer) + project_viewers[str(project_id)].append(viewer) + project_viewers[str(project_id)] = filtered_viewers + + +def add_project_viewer(project_id, username): + viewer = { + 'username': username, + 'cursor': { + 'fileId': None, + 'position': None, + 'added': False + }, + 'cmdFilePath': None, + 'consoleRunPid': None + } + if not str(project_id) in project_viewers.keys(): + project_viewers[str(project_id)] = [viewer] + elif not has_viewer(username, project_id): + project_viewers[str(project_id)].append(viewer) + else: + set_viewer(viewer, project_id) + + +def remove_project_viewer(project_id, username): + if str(project_id) in project_viewers.keys() and \ + has_viewer(username, project_id): + viewers = [] + for viewer in project_viewers[str(project_id)]: + if viewer['username'] != username: + viewers.append(viewer) + project_viewers[str(project_id)] = viewers + + +def is_function_def(cmd): + return len(cmd) > 3 and cmd[0:3] == 'def' + + +@socketio.on('joinUserRoom') +def on_join_user_room(data): + data = json.loads(data) + user_id = data['userId'] + room = 'user' + str(user_id) + join_room(room) + emit('joinUserRoom', { + 'on': 'joinUserRoom', + 'userId': user_id + }, room=room) + + +@socketio.on('leaveUserRoom') +def on_leave_user_room(data): + data = json.loads(data) + user_id = data['userId'] + room = 'user' + str(user_id) + leave_room(room) + emit('leaveUserRoom', { + 'on': 'leaveUserRoom', + 'userId': user_id + }, room=room) + + +@socketio.on('joinProjectRoom') +def on_join_project_room(data): + data = json.loads(data) + username = data['username'] + project_id = data['projectId'] + room = 'project' + str(project_id) + join_room(room) + emit('joinProjectRoom', { + 'on': 'joinProjectRoom', + 'username': username, + 'projectId': project_id + }, room=room) + + +@socketio.on('leaveProjectRoom') +def on_leave_project_room(data): + data = json.loads(data) + username = data['username'] + project_id = data['projectId'] + room = 'project' + str(project_id) + leave_room(room) + emit('leaveProjectRoom', { + 'on': 'leaveProjectRoom', + 'username': username, + 'projectId': project_id + }, room=room) + + +@socketio.on('projectCreated') +def handle_project_created(data): + project = json.loads(data) + emit('projectCreated', { + 'on': 'projectCreated', + 'project': project + }, room='project' + str(project['id'])) + + +@socketio.on('projectChanged') +def handle_project_changed(data): + project = json.loads(data) + emit('projectChanged', { + 'on': 'projectChanged', + 'project': project + }, room='project' + str(project['id'])) + + +@socketio.on('projectDeleted') +def handle_project_deleted(data): + project_id = json.loads(data) + emit('projectDeleted', { + 'on': 'projectDeleted', + 'projectId': project_id + }, room='project' + str(project_id)) + + +@socketio.on('collaboratorAdded') +def handle_collaborator_added(data): + collaborator = json.loads(data) + emit('collaboratorAdded', { + 'on': 'collaboratorAdded', + 'collaborator': collaborator + }, room='user' + str(collaborator['userId'])) + + +@socketio.on('collaboratorChanged') +def handle_collaborator_changed(data): + collaborator = json.loads(data) + emit('collaboratorChanged', { + 'on': 'collaboratorChanged', + 'collaborator': collaborator + }, room='project' + str(collaborator['projectId'])) + + +@socketio.on('joinOpenProjectRoom') +def on_join_open_project_room(data): + data = json.loads(data) + project_id = data['projectId'] + username = data['username'] + room = 'openProject' + str(project_id) + join_room(room) + add_project_viewer(project_id, username) + if not os.path.exists(str(project_id)): + with SimpleFlock(str(project_id) + 'lock'): + if not os.path.exists(str(project_id)): + os.mkdir(str(project_id)) + for file in db.get_project_files(project_id): + dir_name = str(project_id) + ('/' + file['parent'] if file['parent'] else '') + if not os.path.exists(dir_name): + os.makedirs(dir_name) + if not file['isFolder']: + f = open(dir_name + '/' + file['name'], "w+") + f.write(file['content']) + f.close() + emit('joinOpenProjectRoom', { + 'on': 'joinOpenProjectRoom', + 'username': username, + 'viewers': project_viewers[str(project_id)] + }, room=room) + + +@socketio.on('leaveOpenProjectRoom') +def on_leave_open_project_room(data): + data = json.loads(data) + username = data['username'] + project_id = data['projectId'] + room = 'openProject' + str(project_id) + leave_room(room) + + viewer = get_viewer(username, project_id) + if viewer['cmdFilePath']: + os.remove(viewer['cmdFilePath']) + remove_project_viewer(project_id, username) + + if not has_viewers(project_id) and os.path.exists(str(project_id)): + shutil.rmtree(str(project_id)) + + emit('leaveOpenProjectRoom', { + 'on': 'leaveOpenProjectRoom', + 'username': username, + 'viewers': project_viewers[str(project_id)] + }, room=room) + + +@socketio.on('fileCreated') +def handle_file_created(data): + file = json.loads(data) + emit('fileCreated', { + 'on': 'fileCreated', + 'file': file + }, room='openProject' + str(file['projectId'])) + + +@socketio.on('fileChanged') +def handle_file_changed(data): + file = json.loads(data) + emit('fileChanged', { + 'on': 'fileChanged', + 'file': file + }, room='openProject' + str(file['projectId'])) + + +@socketio.on('fileContentChanged') +def handle_file_content_changed(data): + data = json.loads(data) + project_id = data['projectId'] + file_id = data['fileId'] + patches_text = data['patchesText'] + patches = dmp.patch_fromText(patches_text) + + file = db.get_file(file_id) + file.content = dmp.patch_apply(patches, file.content)[0] + db.update_file(file) + + # Update file locally on server if project is open + open_project_path = str(file.projectId) + '/' + if os.path.exists(open_project_path): + if file.parent: + file_path = open_project_path + file.parent + '/' + file.name + else: + file_path = open_project_path + file.name + if os.path.exists(file_path): + f = open(file_path, 'w') + f.write(file.content) + f.close() + emit('fileContentChanged', { + 'on': 'fileContentChanged', + 'projectId': project_id, + 'fileId': file_id, + 'patchesText': patches_text + }, room='openProject' + str(project_id), include_self=False) + + +@socketio.on('fileDeleted') +def handle_file_deleted(data): + file = json.loads(data) + emit('fileDeleted', { + 'on': 'fileDeleted', + 'file': file + }, room='openProject' + str(file['projectId'])) + + +@socketio.on('message') +def handle_message(data): + data = json.loads(data) + + projectId = data['projectId'] + username = data['username'] + message = data['message'] + + author = db.get_user_from_username(username) + new_message = db.Message( + projectId, + author.id, + message + ) + db.add_message(new_message) + + emit('message', { + 'on': 'message', + 'author': db.user_schema.dumps(author), + 'time': new_message.time, + 'message': message + }, room='openProject' + str(projectId)) + + +@socketio.on('viewerCursorChanged') +def handle_viewer_cursor_changed(data): + data = json.loads(data) + project_id = data['projectId'] + file_id = data['fileId'] + username = data['username'] + cursor_position = data['cursorPosition'] + + if not str(project_id) in project_viewers.keys() \ + or not has_viewer(username, project_id): + add_project_viewer(project_id, username) + + for viewer in project_viewers[str(project_id)]: + if viewer['username'] == username: + viewer['cursor'] = { + 'fileId': file_id, + 'position': cursor_position + } + emit('viewerCursorChanged', { + 'on': 'viewerCursorChanged', + 'viewer': username, + 'fileId': file_id, + 'cursorPosition': cursor_position + }, room='openProject' + str(project_id), include_self=False) + + +@socketio.on('lintResult') +def handle_lint_result(data): + data = json.loads(data) + project_id = data['projectId'] + emit('lintResult', { + 'on': 'lintResult', + 'projectId': project_id, + 'lintResult': data['lintResult'], + }, room='openProject' + str(project_id), include_self=False) + + +@socketio.on('runCmd') +def run_cmd(data): + data = json.loads(data) + cmd = data['cmd'] + user_id = data['userId'] + project_id = data['projectId'] + + user = db.get_user(user_id) + viewer = get_viewer(user.username, project_id) + if not viewer['cmdFilePath']: + viewer['cmdFilePath'] = 'temp/runCmd' + user.username + \ + str(math.floor(datetime.now().timestamp())) + '.py' + + f = open(viewer['cmdFilePath'], 'a+') + f.seek(0) + initial_content = f.read() + reset_content = False + f.write(cmd) + f.close() + + p = subprocess.Popen(['python3', viewer['cmdFilePath']], + stdout=PIPE, stderr=PIPE, preexec_fn=os.setsid) + viewer['consoleRunPid'] = p.pid + terminated_normally = True + + while True: + line = p.stdout.readline().decode('utf-8') + if line: + emit('consoleOutput', { + 'on': 'consoleOutput', + 'consoleOutput': line.rstrip(), + 'successful': True + }, room='user' + str(user_id)) + else: + while True: + err = p.stderr.readline().decode('utf-8') + if err: + terminated_normally = False + if not reset_content: + f = open(viewer['cmdFilePath'], 'w+') + f.write(initial_content) + f.close() + reset_content = True + emit('consoleOutput', { + 'on': 'consoleOutput', + 'consoleOutput': err.rstrip(), + 'successful': False + }, room='user' + str(user_id)) + else: + break + break + + if not reset_content and not is_function_def(cmd): + f = open(viewer['cmdFilePath'], 'r+') + lines = f.readlines() + f.truncate(0) + f.close() + + f = open(viewer['cmdFilePath'], 'a+') + for line in lines: + if line.lstrip()[0:6] == 'print(': + num_leading_whitespaces = len(line) - len(line.lstrip(' ')) + f.write((num_leading_whitespaces * ' ') + 'pass\n') + else: + f.write(line) + f.close() + + viewer['consoleRunPid'] = None + emit('runningDone', { + 'on': 'runningDone', + 'terminatedNormally': terminated_normally + }, room='user' + str(user_id)) + + +@socketio.on('runFile') +def run_file(data): + data = json.loads(data) + file = data['file'] + user_id = data['userId'] + project_id = file['projectId'] + file_path = str(project_id) + '/' + file_path += file['parent'] + '/' + file['name'] if file['parent'] else file['name'] + + p = Popen(['python3', file_path], + stdout=PIPE, stderr=PIPE, preexec_fn=os.setsid) + user = db.get_user(user_id) + viewer = get_viewer(user.username, project_id) + viewer['consoleRunPid'] = p.pid + termination_status = TERMINATED_NORMALLY + while True: + line = p.stdout.readline().decode('utf-8') + if line: + emit('consoleOutput', { + 'on': 'consoleOutput', + 'consoleOutput': line.rstrip(), + 'successful': True + }, room='user' + str(user_id)) + else: + while True: + err = p.stderr.readline().decode('utf-8') + if err: + termination_status = TERMINATED_ERROR + emit('consoleOutput', { + 'on': 'consoleOutput', + 'consoleOutput': err.rstrip(), + 'successful': False + }, room='user' + str(user_id)) + else: + break + break + + viewer['consoleRunPid'] = None + emit('runningDone', { + 'on': 'runningDone', + 'terminationStatus': termination_status + }, room='user' + str(user_id)) + + +@socketio.on('resetConsoleFile') +def reset_console_file(data): + data = json.loads(data) + user_id = data['userId'] + project_id = data['projectId'] + user = db.get_user(user_id) + viewer = get_viewer(user.username, project_id) + if viewer['cmdFilePath']: + os.remove(viewer['cmdFilePath']) + viewer['cmdFilePath'] = None + + +@socketio.on('cancelRun') +def cancel_run(data): + data = json.loads(data) + user_id = data['userId'] + project_id = data['projectId'] + user = db.get_user(user_id) + viewer = get_viewer(user.username, project_id) + + if viewer['consoleRunPid']: + os.killpg(os.getpgid(viewer['consoleRunPid']), signal.SIGTERM) + viewer['consoleRunPid'] = None + + emit('runningDone', { + 'on': 'runningDone', + 'terminationStatus': TERMINATED_CANCEL + }, room='user' + str(user_id)) diff --git a/backend/14/src_/__init__.py b/backend/14/src_/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9a192eb778624f2d9aaa7ab839d49027721c1c09 --- /dev/null +++ b/backend/14/src_/__init__.py @@ -0,0 +1,28 @@ +import datetime +import json +import os + +from bson.objectid import ObjectId +from flask import Flask +from flask_cors import CORS + + +class JSONEncoder(json.JSONEncoder): + """ Extend json-encoder class """ + + def default(self, o): + if isinstance(o, ObjectId): + return str(o) + if isinstance(o, set): + return list(o) + if isinstance(o, datetime.datetime): + return str(o) + return json.JSONEncoder.default(self, o) + + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.environ.get('SECRET') +app.config['JWT_SECRET_KEY'] = os.environ.get('SECRET') +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(minutes=10) +app.json_encoder = JSONEncoder +CORS(app) diff --git a/backend/app.py b/backend/app.py index 380ea73f37197ce7cd3682c24f7a5a9c3bdbf5b9..82a962f4c737cdf501c888eb7b6b95558e4dc8b4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -233,6 +233,7 @@ def delete_user(): }), BAD_REQUEST_STATUS_CODE) else: db.delete_user_projects(user.id) + db.delete_user_collaborators(user.id) db.delete_editor_settings(user.editorSettingsId) db.delete_user(user.id) return generate_response(jsonify({ diff --git a/backend/database_helper.py b/backend/database_helper.py index ebf49366dc4afad75d57c39d4a38b2a19fd3d0ed..c6a01ae774d5da70f6fcc0cd73556f1132858bb0 100644 --- a/backend/database_helper.py +++ b/backend/database_helper.py @@ -455,10 +455,16 @@ def delete_collaborator(collaborator_id): return collaborator +def delete_user_collaborators(user_id): + num_collaborators_deleted = Collaborator.query.filter_by(userId=user_id).delete() + db.session.commit() + return num_collaborators_deleted + + def delete_project_collaborators(project_id): - num_project_collaborators_deleted = Collaborator.query.filter_by(projectId=project_id).delete() + num_collaborators_deleted = Collaborator.query.filter_by(projectId=project_id).delete() db.session.commit() - return num_project_collaborators_deleted + return num_collaborators_deleted def delete_all_collaborators(): diff --git a/backend/db.sqlite b/backend/db.sqlite index 084815e9a7d91cf91d5a97c974884aa24770b925..1d732d65c4b07ba635883e618ca1c07d86bda580 100644 Binary files a/backend/db.sqlite and b/backend/db.sqlite differ