diff --git a/AUTHORS b/AUTHORS index db108ce63..c6c6c7b5a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,6 +12,7 @@ in alphabetic order by first name - Debajyoti Dasgupta - Hrithik Kumar Verma - Isabell Krisch +- Jan Eisermann - Jatin Jain - Jens-Uwe Grooß - Jörn Ungermann diff --git a/mslib/mscolab/app/__init__.py b/mslib/mscolab/app/__init__.py index 2387c8422..67e4341d1 100644 --- a/mslib/mscolab/app/__init__.py +++ b/mslib/mscolab/app/__init__.py @@ -29,49 +29,43 @@ import sqlalchemy from flask_migrate import Migrate -from flask import Flask +from flask import Flask, current_app import mslib -from flask import render_template, send_from_directory, send_file, url_for, abort +from flask import url_for from flask_sqlalchemy import SQLAlchemy + +from mslib.mscolab.blueprints.docs import DOCS_BP from mslib.mscolab.conf import mscolab_settings from mslib.utils import prefix_route, release_info -from mslib.msui.icons import icons -from mslib.utils.get_content import get_content +from mslib.utils.file_exists import file_exists from xstatic.main import XStatic -message, update = release_info.check_for_new_release() -if update: - logging.warning(message) - - -def file_exists(filepath=None): - try: - return os.path.isfile(filepath) - except TypeError: - return False - DOCS_SERVER_PATH = os.path.dirname(os.path.abspath(mslib.__file__)) -DOCS_STATIC_DIR = os.path.join(DOCS_SERVER_PATH, 'static') +DOCS_BLUEPRINTS_DIR = os.path.join(DOCS_SERVER_PATH, 'blueprints') +DOCS_STATIC_DIR = os.path.join(DOCS_BLUEPRINTS_DIR, 'static') DOCS_IMG_DIR = os.path.join(DOCS_STATIC_DIR, 'img') DOCS_DOCS_DIR = os.path.join(DOCS_STATIC_DIR, 'docs') +DOCS_TEMPLATES_DIR = os.path.join(DOCS_STATIC_DIR, 'templates') # This can be used to set a location by SCRIPT_NAME for testing. e.g. export SCRIPT_NAME=/demo/ SCRIPT_NAME = os.environ.get('SCRIPT_NAME', '/') + +message, update = release_info.check_for_new_release() +if update: + logging.warning(message) + + # in memory database for testing # app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' -APP = Flask(__name__, template_folder=os.path.join(DOCS_STATIC_DIR, 'templates')) +APP = Flask(__name__, template_folder=os.path.join(DOCS_TEMPLATES_DIR)) APP.config.from_object(mscolab_settings) # Expose docs path for callers/tests and make it part of Flask config for consistency. APP.config['DOCS_SERVER_PATH'] = DOCS_SERVER_PATH APP.route = prefix_route(APP.route, SCRIPT_NAME) -APP.jinja_env.globals.update(file_exists=file_exists) -APP.jinja_env.globals["imprint"] = APP.config['IMPRINT'] -APP.jinja_env.globals["gdpr"] = APP.config['GDPR'] - def _xstatic(name): mod_names = [ @@ -102,86 +96,9 @@ def create_app(imprint=None, gdpr=None): APP.jinja_env.globals.update(file_exists=file_exists) APP.jinja_env.globals["imprint"] = imprint_file APP.jinja_env.globals["gdpr"] = gdpr_file - - @APP.route('/xstatic//') - def files(name, filename): - base_path = _xstatic(name) - if base_path is None: - abort(404) - if not filename: - abort(404) - return send_from_directory(base_path, filename) - - @APP.route('/mss_theme/img/') - def mss_theme(filename): - base_path = os.path.join(DOCS_IMG_DIR) - return send_from_directory(base_path, filename) - APP.jinja_env.globals.update(get_topmenu=get_topmenu) - @APP.route("/index") - def index(): - return render_template("/index.html") - - @APP.route("/mss/about") - @APP.route("/mss") - def about(): - _file = os.path.join(DOCS_DOCS_DIR, 'about.md') - img_url = url_for('overview') - md_overrides = ('![image](/mss/overview.png)', f'![image]({img_url})') - - html_overrides = ('image', - 'image') - content = get_content(_file, md_overrides=md_overrides, html_overrides=html_overrides) - return render_template("/content.html", act="about", content=content) - - @APP.route("/mss/install") - def install(): - _file = os.path.join(DOCS_DOCS_DIR, 'installation.md') - content = get_content(_file) - return render_template("/content.html", act="install", content=content) - - @APP.route("/mss/help") - def help(): # noqa: A001 - _file = os.path.join(DOCS_DOCS_DIR, 'help.md') - html_overrides = ('Waypoint Tutorial', - 'Waypoint Tutorial') - content = get_content(_file, html_overrides=html_overrides) - return render_template("/content.html", act="help", content=content) - - @APP.route("/mss/imprint") - def imprint(): - if file_exists(imprint_file): - content = get_content(imprint_file) - return render_template("/content.html", act="imprint", content=content) - else: - return "" - - @APP.route("/mss/gdpr") - def gdpr(): - if file_exists(gdpr_file): - content = get_content(gdpr_file) - return render_template("/content.html", act="gdpr", content=content) - else: - return "" - - @APP.route('/mss/favicon.ico') - def favicons(): - base_path = icons("16x16", "favicon.ico") - return send_file(base_path) - - @APP.route('/mss/logo.png') - def logo(): - base_path = icons("64x64", "mss-logo.png") - return send_file(base_path) - - @APP.route('/mss/overview.png') - def overview(): - base_path = os.path.join(DOCS_IMG_DIR, 'wise12_overview.png') - return send_file(base_path) - + APP.register_blueprint(DOCS_BP) return APP @@ -205,10 +122,10 @@ def overview(): def get_topmenu(): menu = [ - (url_for('index'), 'Mission Support System', - ((url_for('about'), 'About'), - (url_for('install'), 'Install'), - (url_for('help'), 'Help'), + (url_for('docs.index'), 'Mission Support System', + ((url_for('docs.about'), 'About'), + (url_for('docs.install'), 'Install'), + (url_for('docs.help'), 'Help'), )), ] return menu diff --git a/mslib/mscolab/blueprints/auth/__init__.py b/mslib/mscolab/blueprints/auth/__init__.py new file mode 100644 index 000000000..f7b38698f --- /dev/null +++ b/mslib/mscolab/blueprints/auth/__init__.py @@ -0,0 +1,358 @@ +# -*- coding: utf-8 -*- +""" + + mslib.mscolab.blueprints.auth + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Auth Blueprint for server for mscolab module + + This file is part of MSS. + + :copyright: Copyright 2019 Shivashis Padhi + :copyright: Copyright 2019-2026 by the MSS team, see AUTHORS. + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import datetime +import json +import logging +import secrets + +from flask import Blueprint, request, url_for, render_template, jsonify, flash, redirect +from flask_httpauth import HTTPBasicAuth +from flask.wrappers import Response +from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST +from saml2.metadata import create_metadata_string + +from mslib.mscolab.conf import setup_saml2_backend +from mslib.mscolab.forms import ResetPasswordForm, ResetRequestForm +from mslib.mscolab.models import User +from mslib.mscolab.app import APP +from mslib.utils import conditional_decorator +from mslib.utils.auth import check_login, register_user, generate_confirmation_token, send_email, confirm_token, \ + get_idp_entity_id, create_or_update_idp_user + +AUTH_BP = Blueprint('auth', __name__, template_folder='templates') + +auth_basic_auth = HTTPBasicAuth() + + +@AUTH_BP.route("/status") +@conditional_decorator(auth_basic_auth.login_required, APP.__dict__.get('enable_basic_http_authentication', False)) +def hello(): + if request.authorization is not None: + if APP.__dict__.get('enable_basic_http_authentication', False): + auth_basic_auth.login_required() + return json.dumps({ + 'message': "Mscolab server", + 'use_saml2': APP.config['USE_SAML2'], + 'direct_login': APP.DIRECT_LOGIN + }) + return json.dumps({ + 'message': "Mscolab server", + 'use_saml2': APP.config['USE_SAML2'], + 'direct_login': APP.config['DIRECT_LOGIN'] + }) + else: + return json.dumps({ + 'message': "Mscolab server", + 'use_saml2': APP.config['USE_SAML2'], + 'direct_login': APP.config['DIRECT_LOGIN'] + }) + + +@AUTH_BP.route('/token', methods=["POST"]) +@conditional_decorator(auth_basic_auth.login_required, APP.__dict__.get('enable_basic_http_authentication', False)) +def get_auth_token(): + emailid = request.form['email'] + password = request.form['password'] + user = check_login(emailid, password) + if user is not False: + if APP.config['MAIL_ENABLED']: + if user.confirmed: + token = user.generate_auth_token() + return json.dumps({ + 'token': token, + 'user': {'username': user.username, 'id': user.id, 'fullname': user.fullname}}) + else: + return "False" + else: + token = user.generate_auth_token() + return json.dumps({ + 'token': token, + 'user': {'username': user.username, 'id': user.id, 'fullname': user.fullname}}) + else: + logging.debug("Unauthorized user: %s", emailid) + return "False" + + +@AUTH_BP.route('/test_authorized') +def authorized(): + token = request.args.get('token', request.form.get('token')) + user = User.verify_auth_token(token) + if user is not None: + if APP.config['MAIL_ENABLED']: + if user.confirmed is False: + return "False" + else: + return "True" + else: + return "True" + else: + return "False" + + +@AUTH_BP.route("/register", methods=["POST"]) +@conditional_decorator(auth_basic_auth.login_required, APP.__dict__.get('enable_basic_http_authentication', False)) +def user_register_handler(): + email = request.form['email'] + password = request.form['password'] + username = request.form['username'] + fullname = request.form['fullname'] + result = register_user(email, password, username, fullname) + status_code = 200 + try: + if result["success"]: + status_code = 201 + if APP.config['MAIL_ENABLED']: + status_code = 204 + token = generate_confirmation_token(email) + confirm_url = url_for('auth.user.confirm_email', token=token, _external=True) + html = render_template('auth/user/activate.html', username=username, confirm_url=confirm_url) + subject = "MSColab Please confirm your email" + send_email(email, subject, html) + except TypeError: + result, status_code = {"success": False}, 401 + return jsonify(result), status_code + + +@AUTH_BP.route('/confirm/') +def confirm_email(token): + if APP.config['MAIL_ENABLED']: + try: + email = confirm_token(token) + except TypeError: + return jsonify({"success": False}), 401 + if email is False: + return jsonify({"success": False}), 401 + user = User.query.filter_by(emailid=email).first_or_404() + if user.confirmed: + return render_template('auth/user/confirmed.html', username=user.username) + else: + from mslib.mscolab.server import getConfig + fm = getConfig()[3] + fm.modify_user(user, attribute="confirmed_on", value=datetime.datetime.now(tz=datetime.timezone.utc)) + fm.modify_user(user, attribute="confirmed", value=True) + return render_template('auth/user/confirmed.html', username=user.username) + else: + logging.warning("To send emails, the value of MAIL_ENABLED in conf.py should be set to True.") + return render_template('auth/errors/403.html'), 403 + +@AUTH_BP.route('/reset_password/', methods=['GET', 'POST']) +def reset_password(token): + try: + email = confirm_token(token, expiration=86400) + except TypeError: + return jsonify({"success": False}), 401 + if email is False: + flash("Sorry, your token has expired or is invalid! We will need to resend your authentication email", + 'category_info') + return render_template('auth/user/status_password.html', uri={"path": "reset_request", "name": "Resend " + "authentication " + "email"}) + user = User.query.filter_by(emailid=email).first_or_404() + form = ResetPasswordForm() + if form.validate_on_submit(): + try: + from mslib.mscolab.server import getConfig + fm = getConfig()[3] + user.hash_password(form.confirm_password.data) + fm.modify_user(user, "confirmed", True) + flash('Password reset Success. Please login by the user interface.', 'category_success') + return render_template('auth/user/status_password.html') + except IOError: + flash('Password reset failed. Please try again later', 'category_danger') + return render_template('auth/user/reset_password.html', form=form) + + +@AUTH_BP.route("/reset_request", methods=['GET', 'POST']) +def reset_request(): + if APP.config['MAIL_ENABLED']: + form = ResetRequestForm() + if form.validate_on_submit(): + # Check whether user exists or not based on the db + user = User.query.filter_by(emailid=form.email.data).first() + if user: + try: + username = user.username + token = generate_confirmation_token(form.email.data) + reset_password_url = url_for('auth.user.reset_password', token=token, _external=True) + html = render_template('auth/user/reset_confirmation.html', + reset_password_url=reset_password_url, username=username) + subject = "MSColab Password reset request" + send_email(form.email.data, subject, html) + flash('An email was sent if this user account exists', 'category_success') + return render_template('auth/user/status_password.html') + except IOError: + flash('''We apologize, but it seems that there was an issue sending + your request email. Please try again later.''', 'category_info') + else: + flash('An email was sent if this user account exists', 'category_success') + return render_template('auth/user/status_password.html') + return render_template('auth/user/reset_request.html', form=form) + else: + logging.warning("To send emails, the value of `MAIL_ENABLED` in `conf.py` should be set to True.") + return render_template('auth/errors/403.html'), 403 + + +if APP.config['USE_SAML2']: + # setup idp login config + setup_saml2_backend() + + # set routes for SSO + @AUTH_BP.route('/available_idps/', methods=['GET']) + def available_idps(): + """ + This function checks if IDP (Identity Provider) is enabled in the mscolab_settings module. + If IDP is enabled, it retrieves the configured IDPs from setup_saml2_backend.CONFIGURED_IDPS + and renders the 'idp/available_idps.html' template with the list of configured IDPs. + """ + configured_idps = setup_saml2_backend.CONFIGURED_IDPS + return render_template('auth/idp/available_idps.html', configured_idps=configured_idps), 200 + + @AUTH_BP.route("/idp_login/", methods=['POST']) + def idp_login(): + """Handle the login process for the user by selected IDP""" + selected_idp = request.form.get('selectedIdentityProvider') + sp_config = None + for config in setup_saml2_backend.CONFIGURED_IDPS: + if selected_idp == config['idp_identity_name']: + sp_config = config['idp_data']['saml2client'] + break + + try: + _, response_binding = sp_config.config.getattr("endpoints", "sp")[ + "assertion_consumer_service" + ][0] + entity_id = get_idp_entity_id(selected_idp) + _, binding, http_args = sp_config.prepare_for_negotiated_authenticate( + entityid=entity_id, + response_binding=response_binding, + ) + if binding == BINDING_HTTP_REDIRECT: + headers = dict(http_args["headers"]) + return redirect(str(headers["Location"]), code=303) + return Response(http_args["data"], headers=http_args["headers"]) + except (NameError, AttributeError): + return render_template('auth/errors/403.html'), 403 + + def create_acs_post_handler(config): + """ + Create acs_post_handler function for the given idp_config. + """ + def acs_post_handler(): + """ + Function to handle SAML authentication response. + """ + try: + outstanding_queries = {} + binding = BINDING_HTTP_POST + authn_response = config['idp_data']['saml2client'].parse_authn_request_response( + request.form["SAMLResponse"], binding, outstanding=outstanding_queries + ) + email = None + username = None + + try: + email = authn_response.ava["email"][0] + username = authn_response.ava["givenName"][0] + token = generate_confirmation_token(email) + except (NameError, AttributeError, KeyError): + try: + # Initialize an empty dictionary to store attribute values + attributes = {} + + # Loop through attribute statements + for attribute_statement in authn_response.assertion.attribute_statement: + for attribute in attribute_statement.attribute: + attribute_name = attribute.name + attribute_value = \ + attribute.attribute_value[0].text if attribute.attribute_value else None + attributes[attribute_name] = attribute_value + + # Extract the email and givenname attributes + email = attributes["email"] + username = attributes["givenName"] + token = generate_confirmation_token(email) + except (NameError, AttributeError, KeyError): + return render_template('auth/errors/403.html'), 403 + + if email is not None and username is not None: + idp_user_db_state = create_or_update_idp_user(email, + username, token, idp_config['idp_identity_name']) + if idp_user_db_state: + return render_template('auth/idp/idp_login_success.html', token=token), 200 + return render_template('auth/errors/500.html'), 500 + return render_template('auth/errors/500.html'), 500 + except (NameError, AttributeError, KeyError): + return render_template('auth/errors/403.html'), 403 + return acs_post_handler + + # Implementation for handling configured SAML assertion consumer endpoints + for idp_config in setup_saml2_backend.CONFIGURED_IDPS: + try: + for assertion_consumer_endpoint in idp_config['idp_data']['assertion_consumer_endpoints']: + # Dynamically add the route for the current endpoint + APP.add_url_rule(f'/{assertion_consumer_endpoint}/', assertion_consumer_endpoint, + create_acs_post_handler(idp_config), methods=['POST']) + except (NameError, AttributeError, KeyError) as ex: + logging.warning("USE_SAML2 is %s, Failure is: %s", APP.config['USE_SAML2'], ex) + + @AUTH_BP.route('/idp_login_auth/', methods=['POST']) + def idp_login_auth(): + """Handle the SAML authentication validation of client application.""" + try: + data = request.get_json() + token = data.get('token') + email = confirm_token(token, expiration=1200) + if email: + user = check_login(email, token) + if user: + from mslib.mscolab.server import getConfig + fm = getConfig()[3] + random_token = secrets.token_hex(16) + user.hash_password(random_token) + fm.modify_user(user, action="update_idp_user") + return json.dumps({ + "success": True, + 'token': random_token, + 'user': {'username': user.username, 'id': user.id, 'emailid': user.emailid} + }) + return jsonify({"success": False}), 401 + return jsonify({"success": False}), 401 + except TypeError: + return jsonify({"success": False}), 401 + + @AUTH_BP.route("/metadata/", methods=['GET']) + def metadata(idp_identity_name): + """Return the SAML metadata XML for the requested IDP""" + for config in setup_saml2_backend.CONFIGURED_IDPS: + if idp_identity_name == config['idp_identity_name']: + sp_config = config['idp_data']['saml2client'] + metadata_string = create_metadata_string( + None, sp_config.config, 4, None, None, None, None, None + ).decode("utf-8") + return Response(metadata_string, mimetype="text/xml") + return render_template('auth/errors/404.html'), 404 diff --git a/mslib/static/templates/errors/403.html b/mslib/mscolab/blueprints/auth/templates/auth/errors/403.html similarity index 100% rename from mslib/static/templates/errors/403.html rename to mslib/mscolab/blueprints/auth/templates/auth/errors/403.html diff --git a/mslib/static/templates/errors/404.html b/mslib/mscolab/blueprints/auth/templates/auth/errors/404.html similarity index 100% rename from mslib/static/templates/errors/404.html rename to mslib/mscolab/blueprints/auth/templates/auth/errors/404.html diff --git a/mslib/static/templates/errors/500.html b/mslib/mscolab/blueprints/auth/templates/auth/errors/500.html similarity index 100% rename from mslib/static/templates/errors/500.html rename to mslib/mscolab/blueprints/auth/templates/auth/errors/500.html diff --git a/mslib/static/templates/idp/available_idps.html b/mslib/mscolab/blueprints/auth/templates/auth/idp/available_idps.html similarity index 100% rename from mslib/static/templates/idp/available_idps.html rename to mslib/mscolab/blueprints/auth/templates/auth/idp/available_idps.html diff --git a/mslib/static/templates/idp/idp_login_success.html b/mslib/mscolab/blueprints/auth/templates/auth/idp/idp_login_success.html similarity index 100% rename from mslib/static/templates/idp/idp_login_success.html rename to mslib/mscolab/blueprints/auth/templates/auth/idp/idp_login_success.html diff --git a/mslib/static/templates/user/activate.html b/mslib/mscolab/blueprints/auth/templates/auth/user/activate.html similarity index 100% rename from mslib/static/templates/user/activate.html rename to mslib/mscolab/blueprints/auth/templates/auth/user/activate.html diff --git a/mslib/static/templates/user/confirmed.html b/mslib/mscolab/blueprints/auth/templates/auth/user/confirmed.html similarity index 100% rename from mslib/static/templates/user/confirmed.html rename to mslib/mscolab/blueprints/auth/templates/auth/user/confirmed.html diff --git a/mslib/static/templates/user/reset_confirmation.html b/mslib/mscolab/blueprints/auth/templates/auth/user/reset_confirmation.html similarity index 100% rename from mslib/static/templates/user/reset_confirmation.html rename to mslib/mscolab/blueprints/auth/templates/auth/user/reset_confirmation.html diff --git a/mslib/static/templates/user/reset_password.html b/mslib/mscolab/blueprints/auth/templates/auth/user/reset_password.html similarity index 100% rename from mslib/static/templates/user/reset_password.html rename to mslib/mscolab/blueprints/auth/templates/auth/user/reset_password.html diff --git a/mslib/static/templates/user/reset_request.html b/mslib/mscolab/blueprints/auth/templates/auth/user/reset_request.html similarity index 100% rename from mslib/static/templates/user/reset_request.html rename to mslib/mscolab/blueprints/auth/templates/auth/user/reset_request.html diff --git a/mslib/static/templates/user/status.html b/mslib/mscolab/blueprints/auth/templates/auth/user/status_password.html similarity index 94% rename from mslib/static/templates/user/status.html rename to mslib/mscolab/blueprints/auth/templates/auth/user/status_password.html index 37f14b586..04c3b03c6 100644 --- a/mslib/static/templates/user/status.html +++ b/mslib/mscolab/blueprints/auth/templates/auth/user/status_password.html @@ -32,7 +32,7 @@

Reset Password

{% if uri is defined %}

Click here to - {{uri.name}} + {{uri.name}}

{% endif %} diff --git a/mslib/mscolab/blueprints/chat/__init__.py b/mslib/mscolab/blueprints/chat/__init__.py new file mode 100644 index 000000000..e79a4771a --- /dev/null +++ b/mslib/mscolab/blueprints/chat/__init__.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +""" + + mslib.mscolab.blueprints.chat + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Chat Blueprint for server for mscolab module + + This file is part of MSS. + + :copyright: Copyright 2019 Shivashis Padhi + :copyright: Copyright 2019-2026 by the MSS team, see AUTHORS. + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import json + +import werkzeug +from flask import Blueprint, request, g, jsonify, abort, send_from_directory + +from mslib.mscolab.app import APP +from mslib.mscolab.message_type import MessageType +from mslib.mscolab.utils import get_message_dict +from mslib.utils.auth import verify_user + +CHAT_BP = Blueprint('chat', __name__) + + +@CHAT_BP.route("/messages", methods=["GET"]) +@verify_user +def messages(): + from mslib.mscolab.server import getConfig + fm = getConfig()[3] + user = g.user + op_id = request.args.get("op_id", request.form.get("op_id", None)) + + if fm.is_member(user.id, op_id): + cm = getConfig()[2] + timestamp = request.args.get("timestamp", request.form.get("timestamp", "1970-01-01T00:00:00+00:00")) + chat_messages = cm.get_messages(op_id, timestamp) + return jsonify({"messages": chat_messages}) + return "False" + + +@CHAT_BP.route("/message_attachment", methods=["POST"]) +@verify_user +def message_attachment(): + user = g.user + op_id = request.form.get("op_id", None) + from mslib.mscolab.server import getConfig + fm = getConfig()[3] + if fm.is_member(user.id, op_id): + file = request.files['file'] + message_type = MessageType(int(request.form.get("message_type"))) + user = g.user + users = fm.fetch_users_without_permission(int(op_id), user.id) + if users is False: + return jsonify({"success": False, "message": "Could not send message. No file uploaded."}) + if file is not None: + static_file_path = fm.upload_file(file, subfolder=str(op_id), include_prefix=True) + if static_file_path is not None: + cm = getConfig()[2] + sockio = getConfig()[1] + new_message = cm.add_message(user, static_file_path, op_id, message_type) + new_message_dict = get_message_dict(new_message) + sockio.emit('chat-message-client', json.dumps(new_message_dict)) + return jsonify({"success": True, "path": static_file_path}) + else: + return "False" + return jsonify({"success": False, "message": "Could not send message. No file uploaded."}) + # normal use case never gets to this + return "False" + + +@CHAT_BP.route('/uploads//', methods=["GET"]) +def uploads(name=None, filename=None): + base_path = APP.config['UPLOAD_FOLDER'] + if name is None: + abort(404) + if filename is None: + abort(404) + return send_from_directory(base_path, werkzeug.security.safe_join("", name, filename)) diff --git a/mslib/mscolab/blueprints/docs/__init__.py b/mslib/mscolab/blueprints/docs/__init__.py new file mode 100644 index 000000000..333b69a80 --- /dev/null +++ b/mslib/mscolab/blueprints/docs/__init__.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" + + mslib.mscolab.blueprints.docs + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Docs Blueprint for app module of mscolab + + This file is part of MSS. + + :copyright: Copyright 2016-2026 by the MSS team, see AUTHORS. + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import os +from functools import wraps + +from flask import Blueprint, abort, send_from_directory, render_template, url_for, send_file, current_app +from flask_httpauth import HTTPBasicAuth + +from mslib.msui.icons import icons +from mslib.utils.file_exists import file_exists +from mslib.utils.get_content import get_content + +DOCS_SERVER_PATH = os.path.dirname(os.path.abspath(__file__)) +DOCS_STATIC_DIR = os.path.join(DOCS_SERVER_PATH, 'static') +DOCS_IMG_DIR = os.path.join(DOCS_STATIC_DIR, 'img') +DOCS_DOCS_DIR = os.path.join(DOCS_STATIC_DIR, 'docs') + +DOCS_BP = Blueprint("docs", __name__, template_folder='templates', static_folder='static', static_url_path='/static') +auth_basic_auth = HTTPBasicAuth() + + +def optional_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + if current_app.config.get('enable_basic_http_authentication', False): + return auth_basic_auth.login_required(f)(*args, **kwargs) + return f(*args, **kwargs) + + return decorated + + +@DOCS_BP.route('/') +@optional_auth +def home(): + return render_template("docs/index.html") + + +@DOCS_BP.route('/xstatic//') +def files(name, filename): + from mslib.mscolab.app import _xstatic + base_path = _xstatic(name) + if base_path is None: + abort(404) + if not filename: + abort(404) + return send_from_directory(base_path, filename) + + +@DOCS_BP.route('/mss_theme/img/') +def mss_theme(filename): + base_path = os.path.join(DOCS_IMG_DIR) + return send_from_directory(base_path, filename) + + +@DOCS_BP.route("/index") +def index(): + return render_template("docs/index.html") + + +@DOCS_BP.route("/mss/about") +@DOCS_BP.route("/mss") +def about(): + _file = os.path.join(DOCS_DOCS_DIR, 'about.md') + img_url = url_for('docs.overview') + md_overrides = ('![image](/mss/overview.png)', f'![image]({img_url})') + + html_overrides = ('image', + 'image') + content = get_content(_file, md_overrides=md_overrides, html_overrides=html_overrides) + return render_template("docs/content.html", act="about", content=content) + + +@DOCS_BP.route("/mss/install") +def install(): + _file = os.path.join(DOCS_DOCS_DIR, 'installation.md') + content = get_content(_file) + return render_template("docs/content.html", act="install", content=content) + + +@DOCS_BP.route("/mss/help") +def help(): # noqa: A001 + _file = os.path.join(DOCS_DOCS_DIR, 'help.md') + html_overrides = ('Waypoint Tutorial', + 'Waypoint Tutorial') + content = get_content(_file, html_overrides=html_overrides) + return render_template("docs/content.html", act="help", content=content) + + +@DOCS_BP.route("/mss/imprint") +def imprint(): + imprint_file = current_app.config.get('IMPRINT', None) + if imprint_file is not None and file_exists(imprint_file): + content = get_content(imprint_file) + return render_template("docs/content.html", act="imprint", content=content) + else: + return "" + + +@DOCS_BP.route("/mss/gdpr") +def gdpr(): + gdpr_file = current_app.config.get('GDPR', None) + if gdpr_file is not None and file_exists(gdpr_file): + content = get_content(gdpr_file) + return render_template("docs/content.html", act="gdpr", content=content) + else: + return "" + + +@DOCS_BP.route('/mss/favicon.ico') +def favicons(): + base_path = icons("16x16", "favicon.ico") + return send_file(base_path) + + +@DOCS_BP.route('/mss/logo.png') +def logo(): + base_path = icons("64x64", "mss-logo.png") + return send_file(base_path) + + +@DOCS_BP.route('/mss/overview.png') +def overview(): + base_path = os.path.join(DOCS_IMG_DIR, 'wise12_overview.png') + return send_file(base_path) diff --git a/mslib/static/docs/about.md b/mslib/mscolab/blueprints/docs/static/docs/about.md similarity index 100% rename from mslib/static/docs/about.md rename to mslib/mscolab/blueprints/docs/static/docs/about.md diff --git a/mslib/static/docs/help.md b/mslib/mscolab/blueprints/docs/static/docs/help.md similarity index 100% rename from mslib/static/docs/help.md rename to mslib/mscolab/blueprints/docs/static/docs/help.md diff --git a/mslib/static/docs/installation.md b/mslib/mscolab/blueprints/docs/static/docs/installation.md similarity index 100% rename from mslib/static/docs/installation.md rename to mslib/mscolab/blueprints/docs/static/docs/installation.md diff --git a/mslib/static/img/README b/mslib/mscolab/blueprints/docs/static/img/README similarity index 100% rename from mslib/static/img/README rename to mslib/mscolab/blueprints/docs/static/img/README diff --git a/mslib/static/img/cirrus_hl_2021.png b/mslib/mscolab/blueprints/docs/static/img/cirrus_hl_2021.png similarity index 100% rename from mslib/static/img/cirrus_hl_2021.png rename to mslib/mscolab/blueprints/docs/static/img/cirrus_hl_2021.png diff --git a/mslib/static/img/emerge_2017.jpeg b/mslib/mscolab/blueprints/docs/static/img/emerge_2017.jpeg similarity index 100% rename from mslib/static/img/emerge_2017.jpeg rename to mslib/mscolab/blueprints/docs/static/img/emerge_2017.jpeg diff --git a/mslib/static/img/phileas_2023.jpeg b/mslib/mscolab/blueprints/docs/static/img/phileas_2023.jpeg similarity index 100% rename from mslib/static/img/phileas_2023.jpeg rename to mslib/mscolab/blueprints/docs/static/img/phileas_2023.jpeg diff --git a/mslib/static/img/polstracc_2016.jpeg b/mslib/mscolab/blueprints/docs/static/img/polstracc_2016.jpeg similarity index 100% rename from mslib/static/img/polstracc_2016.jpeg rename to mslib/mscolab/blueprints/docs/static/img/polstracc_2016.jpeg diff --git a/mslib/static/img/publicpreview.jpeg b/mslib/mscolab/blueprints/docs/static/img/publicpreview.jpeg similarity index 100% rename from mslib/static/img/publicpreview.jpeg rename to mslib/mscolab/blueprints/docs/static/img/publicpreview.jpeg diff --git a/mslib/static/img/southtrac_2019.jpeg b/mslib/mscolab/blueprints/docs/static/img/southtrac_2019.jpeg similarity index 100% rename from mslib/static/img/southtrac_2019.jpeg rename to mslib/mscolab/blueprints/docs/static/img/southtrac_2019.jpeg diff --git a/mslib/static/img/stratoclim_2017.jpeg b/mslib/mscolab/blueprints/docs/static/img/stratoclim_2017.jpeg similarity index 100% rename from mslib/static/img/stratoclim_2017.jpeg rename to mslib/mscolab/blueprints/docs/static/img/stratoclim_2017.jpeg diff --git a/mslib/static/img/wise12_overview.png b/mslib/mscolab/blueprints/docs/static/img/wise12_overview.png similarity index 100% rename from mslib/static/img/wise12_overview.png rename to mslib/mscolab/blueprints/docs/static/img/wise12_overview.png diff --git a/mslib/static/img/wise_2017.jpeg b/mslib/mscolab/blueprints/docs/static/img/wise_2017.jpeg similarity index 100% rename from mslib/static/img/wise_2017.jpeg rename to mslib/mscolab/blueprints/docs/static/img/wise_2017.jpeg diff --git a/mslib/static/templates/content.html b/mslib/mscolab/blueprints/docs/templates/docs/content.html similarity index 100% rename from mslib/static/templates/content.html rename to mslib/mscolab/blueprints/docs/templates/docs/content.html diff --git a/mslib/static/templates/index.html b/mslib/mscolab/blueprints/docs/templates/docs/index.html similarity index 100% rename from mslib/static/templates/index.html rename to mslib/mscolab/blueprints/docs/templates/docs/index.html diff --git a/mslib/static/templates/footer.html b/mslib/mscolab/blueprints/docs/templates/footer.html similarity index 92% rename from mslib/static/templates/footer.html rename to mslib/mscolab/blueprints/docs/templates/footer.html index b80c0fd98..7dcda2063 100644 --- a/mslib/static/templates/footer.html +++ b/mslib/mscolab/blueprints/docs/templates/footer.html @@ -17,13 +17,13 @@
{% if file_exists(imprint) %} -
+ {% endif %} {% if file_exists(gdpr) %} -
+ {% endif %} diff --git a/mslib/static/templates/theme.html b/mslib/mscolab/blueprints/docs/templates/theme.html similarity index 70% rename from mslib/static/templates/theme.html rename to mslib/mscolab/blueprints/docs/templates/theme.html index d094472b4..05226bb30 100644 --- a/mslib/static/templates/theme.html +++ b/mslib/mscolab/blueprints/docs/templates/theme.html @@ -8,9 +8,9 @@ - + Mission Support System Collaboration Platform - +
@@ -19,11 +19,12 @@