From fcc0caeb09cbada8e1362f05a935e437c9da3d2e Mon Sep 17 00:00:00 2001 From: Josako Date: Mon, 3 Jun 2024 09:37:59 +0200 Subject: [PATCH] Optimizing admin interface for user domain, completing security views --- common/models/user.py | 1 - common/utils/debug_utils.py | 80 ++++++++----- common/utils/model_utils.py | 0 common/utils/nginx_utils.py | 18 ++- common/utils/security.py | 3 +- common/utils/security_utils.py | 53 +++++++++ config/config.py | 11 +- eveai_app/__init__.py | 8 +- .../templates/basic/confirm_email_fail.html | 16 +++ .../templates/basic/confirm_email_ok.html | 16 +++ eveai_app/templates/email/activate.html | 13 +++ eveai_app/templates/email/reset_password.html | 13 +++ eveai_app/templates/navbar.html | 7 +- .../templates/security/reset_password.html | 14 +-- eveai_app/templates/user/select_tenant.html | 61 +++++----- .../templates/user/view_tenant_domains.html | 61 +++++----- eveai_app/templates/user/view_users.html | 71 ++++++------ eveai_app/views/basic_views.py | 10 ++ eveai_app/views/security_forms.py | 21 ++++ eveai_app/views/security_views.py | 108 +++++++++++++++++- eveai_app/views/user_forms.py | 3 - eveai_app/views/user_views.py | 103 ++++++++++++----- eveai_chat/__init__.py | 2 + requirements.txt | 4 +- 24 files changed, 523 insertions(+), 174 deletions(-) create mode 100644 common/utils/model_utils.py create mode 100644 common/utils/security_utils.py create mode 100644 eveai_app/templates/basic/confirm_email_fail.html create mode 100644 eveai_app/templates/basic/confirm_email_ok.html create mode 100644 eveai_app/templates/email/activate.html create mode 100644 eveai_app/templates/email/reset_password.html create mode 100644 eveai_app/views/security_forms.py diff --git a/common/models/user.py b/common/models/user.py index 9b2ac43..0b148f3 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -104,7 +104,6 @@ class User(db.Model, UserMixin): password = db.Column(db.String(255), nullable=False) first_name = db.Column(db.String(80), nullable=False) last_name = db.Column(db.String(80), nullable=False) - is_active = db.Column(db.Boolean, default=True) active = db.Column(db.Boolean) fs_uniquifier = db.Column(db.String(255), unique=True, nullable=False) confirmed_at = db.Column(db.DateTime, nullable=True) diff --git a/common/utils/debug_utils.py b/common/utils/debug_utils.py index 270b11c..15d48f7 100644 --- a/common/utils/debug_utils.py +++ b/common/utils/debug_utils.py @@ -1,34 +1,62 @@ -from flask import request +from flask import request, session import time +from flask_security import current_user def log_request_middleware(app): - @app.before_request - def log_request_info(): - start_time = time.time() - app.logger.debug(f"Request URL: {request.url}") - app.logger.debug(f"Request Method: {request.method}") - app.logger.debug(f"Request Headers: {request.headers}") - app.logger.debug(f"Time taken for logging request info: {time.time() - start_time} seconds") - try: - app.logger.debug(f"Request Body: {request.get_data()}") - except Exception as e: - app.logger.error(f"Error reading request body: {e}") - app.logger.debug(f"Time taken for logging request body: {time.time() - start_time} seconds") + # @app.before_request + # def log_request_info(): + # start_time = time.time() + # app.logger.debug(f"Request URL: {request.url}") + # app.logger.debug(f"Request Method: {request.method}") + # app.logger.debug(f"Request Headers: {request.headers}") + # app.logger.debug(f"Time taken for logging request info: {time.time() - start_time} seconds") + # try: + # app.logger.debug(f"Request Body: {request.get_data()}") + # except Exception as e: + # app.logger.error(f"Error reading request body: {e}") + # app.logger.debug(f"Time taken for logging request body: {time.time() - start_time} seconds") + + # @app.before_request + # def check_csrf_token(): + # start_time = time.time() + # if request.method == "POST": + # csrf_token = request.form.get("csrf_token") + # app.logger.debug(f"CSRF Token: {csrf_token}") + # app.logger.debug(f"Time taken for logging CSRF token: {time.time() - start_time} seconds") + + # @app.before_request + # def log_user_info(): + # if current_user and current_user.is_authenticated: + # app.logger.debug(f"Before: User ID: {current_user.id}") + # app.logger.debug(f"Before: User Email: {current_user.email}") + # app.logger.debug(f"Before: User Roles: {current_user.roles}") + # else: + # app.logger.debug("After: No user logged in") @app.before_request - def check_csrf_token(): - start_time = time.time() - if request.method == "POST": - csrf_token = request.form.get("csrf_token") - app.logger.debug(f"CSRF Token: {csrf_token}") - app.logger.debug(f"Time taken for logging CSRF token: {time.time() - start_time} seconds") + def log_session_state_before(): + app.logger.debug(f'Session state before request: {session.items()}') + + # @app.after_request + # def log_response_info(response): + # start_time = time.time() + # app.logger.debug(f"Response Status: {response.status}") + # app.logger.debug(f"Response Headers: {response.headers}") + # + # app.logger.debug(f"Time taken for logging response info: {time.time() - start_time} seconds") + # return response + + # @app.after_request + # def log_user_after_request(response): + # if current_user and current_user.is_authenticated: + # app.logger.debug(f"After: User ID: {current_user.id}") + # app.logger.debug(f"after: User Email: {current_user.email}") + # app.logger.debug(f"After: User Roles: {current_user.roles}") + # else: + # app.logger.debug("After: No user logged in") @app.after_request - def log_response_info(response): - start_time = time.time() - app.logger.debug(f"Response Status: {response.status}") - app.logger.debug(f"Response Headers: {response.headers}") - - app.logger.debug(f"Time taken for logging response info: {time.time() - start_time} seconds") - return response \ No newline at end of file + def log_session_state_after(response): + app.logger.debug(f'Session state after request: {session.items()}') + return response diff --git a/common/utils/model_utils.py b/common/utils/model_utils.py new file mode 100644 index 0000000..e69de29 diff --git a/common/utils/nginx_utils.py b/common/utils/nginx_utils.py index dd52ae3..dab1ecb 100644 --- a/common/utils/nginx_utils.py +++ b/common/utils/nginx_utils.py @@ -1,7 +1,19 @@ -from flask import current_app, request, url_for +from flask import request, current_app, url_for +from urllib.parse import urlsplit, urlunsplit def prefixed_url_for(endpoint, **values): prefix = request.headers.get('X-Forwarded-Prefix', '') - current_app.logger.debug(f'prefix: {prefix}') - return prefix + url_for(endpoint, **values) \ No newline at end of file + scheme = request.headers.get('X-Forwarded-Proto', request.scheme) + host = request.headers.get('Host', request.host) + current_app.logger.debug(f'prefix: {prefix}, scheme: {scheme}, host: {host}') + + external = values.pop('_external', False) + generated_url = url_for(endpoint, **values) + + if external: + path, query, fragment = urlsplit(generated_url)[2:5] + new_path = prefix + path + return urlunsplit((scheme, host, new_path, query, fragment)) + else: + return prefix + generated_url \ No newline at end of file diff --git a/common/utils/security.py b/common/utils/security.py index 6624ab9..6aa05fa 100644 --- a/common/utils/security.py +++ b/common/utils/security.py @@ -1,9 +1,10 @@ -from flask import session +from flask import session, current_app from common.models.user import Tenant # Definition of Trigger Handlers def set_tenant_session_data(sender, user, **kwargs): + current_app.logger.debug(f"Setting tenant session data for user {user.id}") tenant = Tenant.query.filter_by(id=user.tenant_id).first() session['tenant'] = tenant.to_dict() session['default_language'] = tenant.default_language diff --git a/common/utils/security_utils.py b/common/utils/security_utils.py new file mode 100644 index 0000000..937c213 --- /dev/null +++ b/common/utils/security_utils.py @@ -0,0 +1,53 @@ +from flask import current_app, render_template +from flask_mailman import EmailMessage +from itsdangerous import URLSafeTimedSerializer + +from common.utils.nginx_utils import prefixed_url_for + + + + + +def confirm_token(token, expiration=3600): + serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) + try: + email = serializer.loads(token, salt=current_app.config['SECURITY_PASSWORD_SALT'], max_age=expiration) + except Exception as e: + current_app.logger.debug(f'Error confirming token: {e}') + raise + return email + + +def send_email(to, subject, template): + msg = EmailMessage(subject=subject, + body=template, + to=[to]) + msg.content_subtype = "html" + msg.send() + + +def generate_reset_token(email): + serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) + return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) + + +def generate_confirmation_token(email): + serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) + return serializer.dumps(email, salt=current_app.config['SECURITY_PASSWORD_SALT']) + + +def send_confirmation_email(user): + current_app.logger.debug(f'Sending confirmation email to {user.email}') + token = generate_confirmation_token(user.email) + confirm_url = prefixed_url_for('security_bp.confirm_email', token=token, _external=True) + current_app.logger.debug(f'Confirmation URL: {confirm_url}') + html = render_template('email/activate.html', confirm_url=confirm_url) + send_email(user.email, "Confirm your email", html) + + +def send_reset_email(user): + token = generate_reset_token(user.email) + reset_url = prefixed_url_for('security_bp.reset_password', token=token, _external=True) + html = render_template('email/reset_password.html', reset_url=reset_url) + send_email(user.email, "Reset Your Password", html) + diff --git a/config/config.py b/config/config.py index 6b39d9b..aaa5486 100644 --- a/config/config.py +++ b/config/config.py @@ -36,8 +36,6 @@ class Config(object): SECURITY_POST_LOGIN_VIEW = '/user/tenant_overview' SECURITY_RECOVERABLE = True SECURITY_EMAIL_SENDER = "eveai_super@flow-it.net" - PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) - SESSION_REFRESH_EACH_REQUEST = True # Ensure Flask-Security-Too is handling CSRF tokens when behind a proxy SECURITY_CSRF_PROTECT_MECHANISMS = ['session'] @@ -70,9 +68,9 @@ class Config(object): CELERY_TIMEZONE = 'UTC' CELERY_ENABLE_UTC = True - # Chunk Definition - MIN_CHUNK_SIZE = 2000 - MAX_CHUNK_SIZE = 3000 + # Chunk Definition, Embedding dependent + O_TE3SMALL_MIN_CHUNK_SIZE = 2000 + O_TE3SMALL_MAX_CHUNK_SIZE = 3000 # LLM TEMPLATES GPT4_SUMMARY_TEMPLATE = """Write a concise summary of the text in the same language as the provided text. @@ -104,7 +102,8 @@ class Config(object): SESSION_TYPE = 'redis' SESSION_PERMANENT = False SESSION_USE_SIGNER = True - SESSION_KEY_PREFIX = 'eveai_chat_' + PERMANENT_SESSION_LIFETIME = timedelta(minutes=60) + SESSION_REFRESH_EACH_REQUEST = True class DevConfig(Config): diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index 1d0f3f5..b1c2768 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -1,6 +1,6 @@ import logging import os -from flask import Flask, render_template, jsonify +from flask import Flask, render_template, jsonify, flash, redirect, request from flask_security import SQLAlchemyUserDatastore, LoginForm from flask_security.signals import user_authenticated from werkzeug.middleware.proxy_fix import ProxyFix @@ -14,6 +14,7 @@ from common.utils.security import set_tenant_session_data from .errors import register_error_handlers from common.utils.celery_utils import make_celery, init_celery from common.utils.debug_utils import log_request_middleware +from common.utils.nginx_utils import prefixed_url_for def create_app(config_file=None): @@ -27,6 +28,8 @@ def create_app(config_file=None): else: app.config.from_object(config_file) + app.config['SESSION_KEY_PREFIX'] = 'eveai_app_' + try: os.makedirs(app.instance_path) except OSError: @@ -67,8 +70,9 @@ def create_app(config_file=None): security_logger.setLevel(logging.DEBUG) sqlalchemy_logger = logging.getLogger('sqlalchemy.engine') sqlalchemy_logger.setLevel(logging.DEBUG) - # log_request_middleware(app) # Add this when debugging nginx or another proxy + log_request_middleware(app) # Add this when debugging nginx or another proxy + # Some generic Error Handling Routines @app.errorhandler(Exception) def handle_exception(e): app.logger.error(f"Unhandled Exception: {e}", exc_info=True) diff --git a/eveai_app/templates/basic/confirm_email_fail.html b/eveai_app/templates/basic/confirm_email_fail.html new file mode 100644 index 0000000..fb99e23 --- /dev/null +++ b/eveai_app/templates/basic/confirm_email_fail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% from "macros.html" import render_field %} + +{% block title %}Email Confirmation OK{% endblock %} + +{% block content_title %}Email Confirmation OK{% endblock %} +{% block content_description %}{% endblock %} + +{% block content %} +Your email cannot be confirmed. The link has expired or is invalid. Please contact your administrator to send you a new confirmation link ;-) +{% endblock %} + + +{% block content_footer %} + +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/basic/confirm_email_ok.html b/eveai_app/templates/basic/confirm_email_ok.html new file mode 100644 index 0000000..41644d9 --- /dev/null +++ b/eveai_app/templates/basic/confirm_email_ok.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% from "macros.html" import render_field %} + +{% block title %}Email Confirmation OK{% endblock %} + +{% block content_title %}Email Confirmation OK{% endblock %} +{% block content_description %}{% endblock %} + +{% block content %} +Your email has been confirmed. You will shortly receive an email to set your password. Then we'll be able to communicate on a deeper level ;-) +{% endblock %} + + +{% block content_footer %} + +{% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/email/activate.html b/eveai_app/templates/email/activate.html new file mode 100644 index 0000000..cb326d8 --- /dev/null +++ b/eveai_app/templates/email/activate.html @@ -0,0 +1,13 @@ + + + + Confirm Your Email + + +

Hi,

+

You have been registered with EveAI by your administrator. Please confirm your email address by clicking the link below:

+

Confirm Email

+

If you were not informed to register, please ignore this email.

+

Thanks,
The EveAI Team

+ + \ No newline at end of file diff --git a/eveai_app/templates/email/reset_password.html b/eveai_app/templates/email/reset_password.html new file mode 100644 index 0000000..7838c41 --- /dev/null +++ b/eveai_app/templates/email/reset_password.html @@ -0,0 +1,13 @@ + + + + Reset Your Password + + +

Hi,

+

You requested a password reset for your EveAI account. Click the link below to reset your password:

+

Reset Password

+

If you did not request a password reset, please ignore this email.

+

Thanks,
The EveAI Team

+ + \ No newline at end of file diff --git a/eveai_app/templates/navbar.html b/eveai_app/templates/navbar.html index b8e6cf9..b745fd3 100644 --- a/eveai_app/templates/navbar.html +++ b/eveai_app/templates/navbar.html @@ -68,14 +68,13 @@