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 @@
{% if current_user.is_authenticated %}
- {{ dropdown('User Mgmt', 'contacts', [
+ {{ dropdown('Tenant Configuration', 'contacts', [
{'name': 'Tenant List', 'url': '/user/select_tenant', 'roles': ['Super User']},
{'name': 'Tenant Registration', 'url': '/user/tenant', 'roles': ['Super User']},
- {'name': 'Generate Chat API Key', 'url': '/user/generate_chat_api_key', 'roles': ['Super User']},
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'Tenant Domains', 'url': '/user/view_tenant_domains/' + session['tenant']['id']|string, 'roles': ['Super User', 'Tenant Admin']},
+ {'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Tenant Domain Registration', 'url': '/user/tenant_domain', 'roles': ['Super User', 'Tenant Admin']},
- {'name': 'User List', 'url': '/user/view_users/' + session['tenant']['id']|string, 'roles': ['Super User', 'Tenant Admin']},
+ {'name': 'User List', 'url': '/user/view_users', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'User Registration', 'url': '/user/user', 'roles': ['Super User', 'Tenant Admin']},
]) }}
{% endif %}
diff --git a/eveai_app/templates/security/reset_password.html b/eveai_app/templates/security/reset_password.html
index 7423f9e..e8c59f2 100644
--- a/eveai_app/templates/security/reset_password.html
+++ b/eveai_app/templates/security/reset_password.html
@@ -1,19 +1,19 @@
{% extends "security/base.html" %}
-{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors, render_form_errors %}
+{% from "macros.html" import render_field %}
{% block title %} {{ _fsdomain('Reset password') }} {% endblock %}
{% block content_title %} {{ _fsdomain('Reset password') }} {% endblock %}
{% block content_description %}An email will be sent to you with instructions.{% endblock %}
{% block content %}
- {% include "security/_messages.html" %}
+{# {% include "security/_messages.html" %}#}
{#
{# {% include "security/_menu.html" %}#}
{% endblock content %}
diff --git a/eveai_app/templates/user/select_tenant.html b/eveai_app/templates/user/select_tenant.html
index 196c13b..78e2526 100644
--- a/eveai_app/templates/user/select_tenant.html
+++ b/eveai_app/templates/user/select_tenant.html
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
-{% from "macros.html" import render_selectable_table %}
+{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}Tenant Selection{% endblock %}
@@ -8,37 +8,44 @@
{% block content %}
{% endblock %}
-{% block scripts %}
-
+{% block content_footer %}
+ {{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% endblock %}
+
+{#{% block scripts %}#}
+{##}
+{#{% endblock %}#}
diff --git a/eveai_app/templates/user/view_tenant_domains.html b/eveai_app/templates/user/view_tenant_domains.html
index 771ac0f..287ba32 100644
--- a/eveai_app/templates/user/view_tenant_domains.html
+++ b/eveai_app/templates/user/view_tenant_domains.html
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
-{% from "macros.html" import render_selectable_table %}
+{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Tenant Domains{% endblock %}
@@ -8,38 +8,43 @@
{% block content %}
{% endblock %}
+
+{% block content_footer %}
+ {{ render_pagination(pagination, 'user_bp.view_tenant_domains') }}
+{% endblock %}
+
{% block scripts %}
-
+{##}
{% endblock %}
\ No newline at end of file
diff --git a/eveai_app/templates/user/view_users.html b/eveai_app/templates/user/view_users.html
index f419489..232f45b 100644
--- a/eveai_app/templates/user/view_users.html
+++ b/eveai_app/templates/user/view_users.html
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
-{% from "macros.html" import render_selectable_table %}
+{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Users{% endblock %}
@@ -8,42 +8,49 @@
{% block content %}
{% endblock %}
+
+{% block content_footer %}
+ {{ render_pagination(pagination, 'user_bp.select_tenant') }}
+{% endblock %}
+
{% block scripts %}
-
+{##}
{% endblock %}
\ No newline at end of file
diff --git a/eveai_app/views/basic_views.py b/eveai_app/views/basic_views.py
index 75133b2..f64c07b 100644
--- a/eveai_app/views/basic_views.py
+++ b/eveai_app/views/basic_views.py
@@ -22,6 +22,16 @@ def index():
return render_template('index.html')
+@basic_bp.route('/confirm_email_ok', methods=['GET', ])
+def confirm_email_ok():
+ return render_template('basic/confirm_email_ok.html')
+
+
+@basic_bp.route('/confirm_email_fail', methods=['GET', ])
+def confirm_email_fail():
+ return render_template('basic/confirm_email_fail.html')
+
+
@basic_bp.route('/session_defaults', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin')
def session_defaults():
diff --git a/eveai_app/views/security_forms.py b/eveai_app/views/security_forms.py
new file mode 100644
index 0000000..71190bb
--- /dev/null
+++ b/eveai_app/views/security_forms.py
@@ -0,0 +1,21 @@
+from flask import current_app
+from flask_wtf import FlaskForm
+from wtforms import PasswordField, SubmitField, StringField
+from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, EqualTo
+
+
+class SetPasswordForm(FlaskForm):
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Set Password')
+
+
+class RequestResetForm(FlaskForm):
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ submit = SubmitField('Request Password Reset')
+
+
+class ResetPasswordForm(FlaskForm):
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Reset Password')
\ No newline at end of file
diff --git a/eveai_app/views/security_views.py b/eveai_app/views/security_views.py
index a982deb..20b9270 100644
--- a/eveai_app/views/security_views.py
+++ b/eveai_app/views/security_views.py
@@ -1,13 +1,20 @@
# views/security_views.py
-from flask import Blueprint, render_template, redirect, request, flash, current_app
+from flask import Blueprint, render_template, redirect, request, flash, current_app, abort, session
from flask_security import current_user, login_required, login_user, logout_user
-from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value
+from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
from flask_security.forms import LoginForm
from urllib.parse import urlparse
-import datetime as dt
+from datetime import datetime as dt, timezone as tz
+
+from itsdangerous import URLSafeTimedSerializer
+from sqlalchemy.exc import SQLAlchemyError
from common.models.user import User
from common.utils.nginx_utils import prefixed_url_for
+from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, RequestResetForm
+from common.extensions import db
+from common.utils.security_utils import confirm_token, send_confirmation_email, send_reset_email
+from common.utils.security import set_tenant_session_data
security_bp = Blueprint('security_bp', __name__)
@@ -15,11 +22,19 @@ security_bp = Blueprint('security_bp', __name__)
@security_bp.before_request
def log_before_request():
current_app.logger.debug(f"Before request (security_bp): {request.method} {request.url}")
+ if current_user and current_user.is_authenticated:
+ current_app.logger.debug(f"After request (security_bp): Current User: {current_user.email}")
+ else:
+ current_app.logger.debug(f"After request (security_bp): No user logged in")
@security_bp.after_request
def log_after_request(response):
current_app.logger.debug(f"After request (security_bp): {request.method} {request.url} - Status: {response.status}")
+ if current_user and current_user.is_authenticated:
+ current_app.logger.debug(f"After request (security_bp): Current User: {current_user.email}")
+ else:
+ current_app.logger.debug(f"After request (security_bp): No user logged in")
return response
@@ -34,11 +49,19 @@ def login():
current_app.logger.debug(f'Validating login form: {form.email.data}')
user = User.query.filter_by(email=form.email.data).first()
if user is None or not verify_and_update_password(form.password.data, user):
- flash('Invalid username or password')
+ flash('Invalid username or password', 'danger')
return redirect(prefixed_url_for('security_bp.login'))
- login_user(user, remember=form.remember.data)
- return redirect(prefixed_url_for('user_bp.tenant_overview'))
+ if login_user(user):
+ current_app.logger.info(f'Login successful! Current User is {current_user.email}')
+ db.session.commit()
+ return redirect(prefixed_url_for('user_bp.tenant_overview'))
+ else:
+ flash('Invalid username or password', 'danger')
+ current_app.logger.debug(f'Failed to login user {user.email}')
+ abort(401)
+ else:
+ current_app.logger.debug(f'Invalid login form: {form.errors}')
return render_template('security/login_user.html', login_user_form=form)
@@ -50,3 +73,76 @@ def logout():
logout_user()
current_app.logger.debug('After Logout')
return redirect(prefixed_url_for('basic_bp.index'))
+
+
+@security_bp.route('/confirm_email/
', methods=['GET', 'POST'])
+def confirm_email(token):
+ try:
+ email = confirm_token(token)
+ except Exception as e:
+ flash('The confirmation link is invalid or has expired.', 'danger')
+ current_app.logger.debug(f'Invalid confirmation link detected: {token} - error: {e}')
+ return redirect(prefixed_url_for('basic_bp.confirm_email_fail'))
+
+ user = User.query.filter_by(email=email).first_or_404()
+ current_app.logger.debug(f'Trying to confirm email for user {user.email}')
+ if user.active:
+ flash('Account already confirmed. Please login.', 'success')
+ current_app.logger.debug(f'Account for user {user.email} was already activated')
+ return redirect(prefixed_url_for('security_bp.login'))
+ else:
+ current_app.logger.debug(f'Trying to confirm email for user {user.email}')
+ user.active = True
+ user.updated_at = dt.now(tz.utc)
+ user.confirmed_at = dt.now(tz.utc)
+
+ try:
+ db.session.add(user)
+ db.session.commit()
+ except SQLAlchemyError as e:
+ db.session.rollback()
+ current_app.logger.debug(f'Failed to confirm email for user {user.email}: {e}')
+ return redirect(prefixed_url_for('basic_bp.confirm_email_fail'))
+
+ current_app.logger.debug(f'Account for user {user.email} was confirmed.')
+ send_reset_email(user)
+ return redirect(prefixed_url_for('basic_bp.confirm_email_ok'))
+
+
+@security_bp.route('/reset_password_request', methods=['GET', 'POST'])
+def reset_password_request():
+ form = RequestResetForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user:
+ send_reset_email(user)
+ flash('An email with instructions to reset your password has been sent.', 'info')
+ return redirect(prefixed_url_for('security_bp.login'))
+ return render_template('security/reset_password_request.html', form=form)
+
+
+@security_bp.route('/reset_password/', methods=['GET', 'POST'])
+def reset_password(token):
+ try:
+ email = confirm_token(token)
+ except Exception as e:
+ flash('The reset link is invalid or has expired.', 'danger')
+ current_app.logger.debug(f'Invalid reset link detected: {token} - error: {e}')
+ return redirect(prefixed_url_for('security_bp.reset_password_request'))
+
+ user = User.query.filter_by(email=email).first_or_404()
+ form = ResetPasswordForm()
+ if form.validate_on_submit():
+ user.password = hash_password(form.password.data)
+ user.updated_at = dt.now(tz.utc)
+ db.session.commit()
+ flash('Your password has been updated.', 'success')
+ return redirect(prefixed_url_for('security_bp.login'))
+ return render_template('security/reset_password.html', reset_password_form=form)
+
+
+
+
+
+
+
diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py
index fbce9c8..9af68bb 100644
--- a/eveai_app/views/user_forms.py
+++ b/eveai_app/views/user_forms.py
@@ -48,7 +48,6 @@ class BaseUserForm(FlaskForm):
email = EmailField('Email', validators=[DataRequired(), Email()])
first_name = StringField('First Name', validators=[DataRequired(), Length(max=80)])
last_name = StringField('Last Name', validators=[DataRequired(), Length(max=80)])
- is_active = BooleanField('Is Active', id='flexSwitchCheckDefault', default=True)
valid_to = DateField('Valid to', id='form-control datepicker', validators=[Optional()])
tenant_id = IntegerField('Tenant ID', validators=[NumberRange(min=0)])
roles = SelectMultipleField('Roles', coerce=int)
@@ -59,8 +58,6 @@ class BaseUserForm(FlaskForm):
class CreateUserForm(BaseUserForm):
- password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
- confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), Length(min=8)])
submit = SubmitField('Create User')
diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py
index 967f973..0855b33 100644
--- a/eveai_app/views/user_views.py
+++ b/eveai_app/views/user_views.py
@@ -3,11 +3,13 @@ import uuid
from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
from flask_security import hash_password, roles_required, roles_accepted, current_user
+from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
import ast
from common.models.user import User, Tenant, Role, TenantDomain
-from common.extensions import db, kms_client
+from common.extensions import db, kms_client, security
+from common.utils.security_utils import send_confirmation_email, send_reset_email
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm
from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro
@@ -20,11 +22,19 @@ user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@user_bp.before_request
def log_before_request():
current_app.logger.debug(f"Before request (user_bp): {request.method} {request.url}")
+ if current_user and current_user.is_authenticated:
+ current_app.logger.debug(f"After request (user_bp): Current User: {current_user.email}")
+ else:
+ current_app.logger.debug(f"After request (user_bp): No user logged in")
@user_bp.after_request
def log_after_request(response):
current_app.logger.debug(f"After request (user_bp): {request.method} {request.url} - Status: {response.status}")
+ if current_user and current_user.is_authenticated:
+ current_app.logger.debug(f"After request (user_bp): Current User: {current_user.email}")
+ else:
+ current_app.logger.debug(f"After request (user_bp): No user logged in")
return response
@@ -119,7 +129,7 @@ def edit_tenant(tenant_id):
@roles_accepted('Super User', 'Tenant Admin')
def user():
form = CreateUserForm()
- form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant
+ form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant
if form.validate_on_submit():
current_app.logger.info(f"Adding User for tenant {session['tenant']['id']} ")
if form.password.data != form.confirm_password.data:
@@ -133,12 +143,10 @@ def user():
password=hashed_password,
first_name=form.first_name.data,
last_name=form.last_name.data,
- is_active=form.is_active.data,
valid_to=form.valid_to.data,
tenant_id=form.tenant_id.data
)
- new_user.fs_uniquifier = str(uuid.uuid4())
timestamp = dt.now(tz.utc)
new_user.created_at = timestamp
new_user.updated_at = timestamp
@@ -158,8 +166,11 @@ def user():
try:
db.session.add(new_user)
db.session.commit()
- current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database')
- flash('User added successfully.', 'success')
+ security.datastore.set_uniquifier()
+ send_confirmation_email(new_user)
+ current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database'
+ f'Confirmation email sent to {new_user.email}')
+ flash('User added successfully and confirmation email sent.', 'success')
return redirect(prefixed_url_for('user_bp.view_users'))
except Exception as e:
current_app.logger.error(f'Failed to add user with name {new_user.user_name}. Error: {str(e)}')
@@ -179,7 +190,6 @@ def edit_user(user_id):
# Populate the user with form data
user.first_name = form.first_name.data
user.last_name = form.last_name.data
- user.is_active = form.is_active.data
user.valid_to = form.valid_to.data
user.updated_at = dt.now(tz.utc)
@@ -200,7 +210,8 @@ def edit_user(user_id):
db.session.commit()
flash('User updated successfully.', 'success')
return redirect(
- prefixed_url_for('user_bp.edit_user', user_id=user.id)) # Assuming there's a user profile view to redirect to
+ prefixed_url_for('user_bp.edit_user',
+ user_id=user.id)) # Assuming there's a user profile view to redirect to
form.roles.data = [role.id for role in user.roles]
return render_template('user/edit_user.html', form=form, user_id=user_id)
@@ -209,11 +220,17 @@ def edit_user(user_id):
@user_bp.route('/select_tenant')
@roles_required('Super User')
def select_tenant():
- tenants = Tenant.query.all() # Fetch all tenants from the database
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 10, type=int)
- prepared_data = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', '')])
+ query = Tenant.query.order_by(Tenant.name) # Fetch all tenants from the database
- return render_template('user/select_tenant.html', tenants=prepared_data)
+ pagination = query.paginate(page=page, per_page=per_page)
+ tenants = pagination.items
+
+ rows = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', '')])
+
+ return render_template('user/select_tenant.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_tenant_selection', methods=['POST'])
@@ -239,17 +256,24 @@ def handle_tenant_selection():
return redirect(prefixed_url_for('select_tenant'))
-@user_bp.route('/view_users/')
+@user_bp.route('/view_users')
@roles_accepted('Super User', 'Tenant Admin')
-def view_users(tenant_id):
- tenant_id = int(tenant_id)
- users = User.query.filter_by(tenant_id=tenant_id).all()
+def view_users():
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 10, type=int)
- prepared_data = prepare_table_for_macro(users, [('id', ''), ('user_name', ''), ('email', '')])
- current_app.logger.debug(f'prepared_data: {prepared_data}')
+ tenant_id = session.get('tenant').get('id')
+ query = User.query.filter_by(tenant_id=tenant_id).order_by(User.user_name)
+
+ pagination = query.paginate(page=page, per_page=per_page)
+ users = pagination.items
+
+ # prepare table data
+
+ rows = prepare_table_for_macro(users, [('id', ''), ('user_name', ''), ('email', '')])
# Render the users in a template
- return render_template('user/view_users.html', users=prepared_data)
+ return render_template('user/view_users.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_user_action', methods=['POST'])
@@ -257,25 +281,38 @@ def view_users(tenant_id):
def handle_user_action():
user_identification = request.form['selected_row']
user_id = ast.literal_eval(user_identification).get('value')
+ user = User.query.get_or_404(user_id)
action = request.form['action']
if action == 'edit_user':
return redirect(prefixed_url_for('user_bp.edit_user', user_id=user_id))
- # Add more conditions for other actions
- return redirect(prefixed_url_for('view_users'))
+ elif action == 'resend_confirmation_email':
+ send_confirmation_email(user)
+ flash(f'Confirmation email sent to {user.email}.', 'success')
+ elif action == 'reset_uniquifier':
+ reset_uniquifier(user)
+ flash(f'Uniquifier reset for {user.user_name}.', 'success')
+
+ return redirect(prefixed_url_for('user_bp.view_users'))
-@user_bp.route('/view_tenant_domains/')
+@user_bp.route('/view_tenant_domains')
@roles_accepted('Super User', 'Tenant Admin')
-def view_tenant_domains(tenant_id):
- tenant_id = int(tenant_id)
- tenant_domains = TenantDomain.query.filter_by(tenant_id=tenant_id).all()
+def view_tenant_domains():
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 10, type=int)
+
+ tenant_id = session.get('tenant').get('id')
+ query = TenantDomain.query.filter_by(tenant_id=tenant_id).order_by(TenantDomain.domain)
+
+ pagination = query.paginate(page=page, per_page=per_page)
+ tenant_domains = pagination.items
# prepare table data
- prepared_data = prepare_table_for_macro(tenant_domains, [('id', ''), ('domain', ''), ('valid_to', '')])
+ rows = prepare_table_for_macro(tenant_domains, [('id', ''), ('domain', ''), ('valid_to', '')])
# Render the users in a template
- return render_template('user/view_tenant_domains.html', tenant_domains=prepared_data)
+ return render_template('user/view_tenant_domains.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_tenant_domain_action', methods=['POST'])
@@ -336,7 +373,8 @@ def edit_tenant_domain(tenant_domain_id):
f'for tenant {session["tenant"]["id"]}'
f'Error: {str(e)}')
return redirect(
- prefixed_url_for('user_bp.view_tenant_domains', tenant_id=session['tenant']['id'])) # Assuming there's a user profile view to redirect to
+ prefixed_url_for('user_bp.view_tenant_domains',
+ tenant_id=session['tenant']['id'])) # Assuming there's a user profile view to redirect to
return render_template('user/edit_tenant_domain.html', form=form, tenant_domain_id=tenant_domain_id)
@@ -374,12 +412,23 @@ def generate_chat_api_key():
@user_bp.route('/tenant_overview', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def tenant_overview():
+ current_app.logger.debug('Rendering tenant overview')
+ current_app.logger.debug(f'current_user: {current_user}')
+ current_app.logger.debug(f'Current user roles: {current_user.roles}')
tenant_id = session['tenant']['id']
+ current_app.logger.debug(f'Generating overview for tenant {tenant_id}')
tenant = Tenant.query.get_or_404(tenant_id)
form = TenantForm(obj=tenant)
return render_template('user/tenant_overview.html', form=form)
+def reset_uniquifier(user):
+ security.datastore.set_uniquifier(user)
+ db.session.add(user)
+ db.session.commit()
+ send_reset_email(user)
+
+
def set_logging_information(obj, timestamp):
obj.created_at = timestamp
obj.updated_at = timestamp
diff --git a/eveai_chat/__init__.py b/eveai_chat/__init__.py
index ca7fc97..fece195 100644
--- a/eveai_chat/__init__.py
+++ b/eveai_chat/__init__.py
@@ -20,6 +20,8 @@ def create_app(config_file=None):
else:
app.config.from_object(config_file)
+ app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_'
+
logging.config.dictConfig(LOGGING)
register_extensions(app)
diff --git a/requirements.txt b/requirements.txt
index 6c24e9c..8b2abc7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,4 +10,6 @@ kombu~=5.3.7
langchain~=0.1.17
requests~=2.31.0
beautifulsoup4~=4.12.3
-google~=3.0.0
\ No newline at end of file
+google~=3.0.0
+redis~=5.0.4
+itsdangerous~=2.2.0
\ No newline at end of file