Optimizing admin interface for user domain, completing security views

This commit is contained in:
Josako
2024-06-03 09:37:59 +02:00
parent e5a36798bf
commit fcc0caeb09
24 changed files with 523 additions and 174 deletions

View File

@@ -104,7 +104,6 @@ class User(db.Model, UserMixin):
password = db.Column(db.String(255), nullable=False) password = db.Column(db.String(255), nullable=False)
first_name = db.Column(db.String(80), nullable=False) first_name = db.Column(db.String(80), nullable=False)
last_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) active = db.Column(db.Boolean)
fs_uniquifier = db.Column(db.String(255), unique=True, nullable=False) fs_uniquifier = db.Column(db.String(255), unique=True, nullable=False)
confirmed_at = db.Column(db.DateTime, nullable=True) confirmed_at = db.Column(db.DateTime, nullable=True)

View File

@@ -1,34 +1,62 @@
from flask import request from flask import request, session
import time import time
from flask_security import current_user
def log_request_middleware(app): def log_request_middleware(app):
@app.before_request # @app.before_request
def log_request_info(): # def log_request_info():
start_time = time.time() # start_time = time.time()
app.logger.debug(f"Request URL: {request.url}") # app.logger.debug(f"Request URL: {request.url}")
app.logger.debug(f"Request Method: {request.method}") # app.logger.debug(f"Request Method: {request.method}")
app.logger.debug(f"Request Headers: {request.headers}") # app.logger.debug(f"Request Headers: {request.headers}")
app.logger.debug(f"Time taken for logging request info: {time.time() - start_time} seconds") # app.logger.debug(f"Time taken for logging request info: {time.time() - start_time} seconds")
try: # try:
app.logger.debug(f"Request Body: {request.get_data()}") # app.logger.debug(f"Request Body: {request.get_data()}")
except Exception as e: # except Exception as e:
app.logger.error(f"Error reading request body: {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.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 @app.before_request
def check_csrf_token(): def log_session_state_before():
start_time = time.time() app.logger.debug(f'Session state before request: {session.items()}')
if request.method == "POST":
csrf_token = request.form.get("csrf_token") # @app.after_request
app.logger.debug(f"CSRF Token: {csrf_token}") # def log_response_info(response):
app.logger.debug(f"Time taken for logging CSRF token: {time.time() - start_time} seconds") # 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 @app.after_request
def log_response_info(response): def log_session_state_after(response):
start_time = time.time() app.logger.debug(f'Session state after request: {session.items()}')
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 return response

View File

View File

@@ -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): def prefixed_url_for(endpoint, **values):
prefix = request.headers.get('X-Forwarded-Prefix', '') prefix = request.headers.get('X-Forwarded-Prefix', '')
current_app.logger.debug(f'prefix: {prefix}') scheme = request.headers.get('X-Forwarded-Proto', request.scheme)
return prefix + url_for(endpoint, **values) 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

View File

@@ -1,9 +1,10 @@
from flask import session from flask import session, current_app
from common.models.user import Tenant from common.models.user import Tenant
# Definition of Trigger Handlers # Definition of Trigger Handlers
def set_tenant_session_data(sender, user, **kwargs): 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() tenant = Tenant.query.filter_by(id=user.tenant_id).first()
session['tenant'] = tenant.to_dict() session['tenant'] = tenant.to_dict()
session['default_language'] = tenant.default_language session['default_language'] = tenant.default_language

View File

@@ -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)

View File

@@ -36,8 +36,6 @@ class Config(object):
SECURITY_POST_LOGIN_VIEW = '/user/tenant_overview' SECURITY_POST_LOGIN_VIEW = '/user/tenant_overview'
SECURITY_RECOVERABLE = True SECURITY_RECOVERABLE = True
SECURITY_EMAIL_SENDER = "eveai_super@flow-it.net" 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 # Ensure Flask-Security-Too is handling CSRF tokens when behind a proxy
SECURITY_CSRF_PROTECT_MECHANISMS = ['session'] SECURITY_CSRF_PROTECT_MECHANISMS = ['session']
@@ -70,9 +68,9 @@ class Config(object):
CELERY_TIMEZONE = 'UTC' CELERY_TIMEZONE = 'UTC'
CELERY_ENABLE_UTC = True CELERY_ENABLE_UTC = True
# Chunk Definition # Chunk Definition, Embedding dependent
MIN_CHUNK_SIZE = 2000 O_TE3SMALL_MIN_CHUNK_SIZE = 2000
MAX_CHUNK_SIZE = 3000 O_TE3SMALL_MAX_CHUNK_SIZE = 3000
# LLM TEMPLATES # LLM TEMPLATES
GPT4_SUMMARY_TEMPLATE = """Write a concise summary of the text in the same language as the provided text. 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_TYPE = 'redis'
SESSION_PERMANENT = False SESSION_PERMANENT = False
SESSION_USE_SIGNER = True SESSION_USE_SIGNER = True
SESSION_KEY_PREFIX = 'eveai_chat_' PERMANENT_SESSION_LIFETIME = timedelta(minutes=60)
SESSION_REFRESH_EACH_REQUEST = True
class DevConfig(Config): class DevConfig(Config):

View File

@@ -1,6 +1,6 @@
import logging import logging
import os 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 import SQLAlchemyUserDatastore, LoginForm
from flask_security.signals import user_authenticated from flask_security.signals import user_authenticated
from werkzeug.middleware.proxy_fix import ProxyFix 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 .errors import register_error_handlers
from common.utils.celery_utils import make_celery, init_celery from common.utils.celery_utils import make_celery, init_celery
from common.utils.debug_utils import log_request_middleware from common.utils.debug_utils import log_request_middleware
from common.utils.nginx_utils import prefixed_url_for
def create_app(config_file=None): def create_app(config_file=None):
@@ -27,6 +28,8 @@ def create_app(config_file=None):
else: else:
app.config.from_object(config_file) app.config.from_object(config_file)
app.config['SESSION_KEY_PREFIX'] = 'eveai_app_'
try: try:
os.makedirs(app.instance_path) os.makedirs(app.instance_path)
except OSError: except OSError:
@@ -67,8 +70,9 @@ def create_app(config_file=None):
security_logger.setLevel(logging.DEBUG) security_logger.setLevel(logging.DEBUG)
sqlalchemy_logger = logging.getLogger('sqlalchemy.engine') sqlalchemy_logger = logging.getLogger('sqlalchemy.engine')
sqlalchemy_logger.setLevel(logging.DEBUG) 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) @app.errorhandler(Exception)
def handle_exception(e): def handle_exception(e):
app.logger.error(f"Unhandled Exception: {e}", exc_info=True) app.logger.error(f"Unhandled Exception: {e}", exc_info=True)

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<title>Confirm Your Email</title>
</head>
<body>
<p>Hi,</p>
<p>You have been registered with EveAI by your administrator. Please confirm your email address by clicking the link below:</p>
<p><a href="{{ confirm_url }}">Confirm Email</a></p>
<p>If you were not informed to register, please ignore this email.</p>
<p>Thanks,<br>The EveAI Team</p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<title>Reset Your Password</title>
</head>
<body>
<p>Hi,</p>
<p>You requested a password reset for your EveAI account. Click the link below to reset your password:</p>
<p><a href="{{ reset_url }}">Reset Password</a></p>
<p>If you did not request a password reset, please ignore this email.</p>
<p>Thanks,<br>The EveAI Team</p>
</body>
</html>

View File

@@ -68,14 +68,13 @@
<div class="collapse navbar-collapse w-100 pt-3 pb-2 py-lg-0" id="navigation"> <div class="collapse navbar-collapse w-100 pt-3 pb-2 py-lg-0" id="navigation">
<ul class="navbar-nav navbar-nav-hover mx-auto"> <ul class="navbar-nav navbar-nav-hover mx-auto">
{% if current_user.is_authenticated %} {% 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 List', 'url': '/user/select_tenant', 'roles': ['Super User']},
{'name': 'Tenant Registration', 'url': '/user/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 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': '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']}, {'name': 'User Registration', 'url': '/user/user', 'roles': ['Super User', 'Tenant Admin']},
]) }} ]) }}
{% endif %} {% endif %}

View File

@@ -1,19 +1,19 @@
{% extends "security/base.html" %} {% 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 title %} {{ _fsdomain('Reset password') }} {% endblock %}
{% block content_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_description %}An email will be sent to you with instructions.{% endblock %}
{% block content %} {% block content %}
{% include "security/_messages.html" %} {# {% include "security/_messages.html" %}#}
{# <form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="post" name="reset_password_form">#} {# <form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="post" name="reset_password_form">#}
<form action="" method="post"> <form action="" method="post">
{{ reset_password_form.hidden_tag() }} {{ reset_password_form.hidden_tag() }}
{{ render_form_errors(reset_password_form) }} {# {{ render_form_errors(reset_password_form) }}#}
{{ render_field_with_errors(reset_password_form.password) }} {{ render_field(reset_password_form.password) }}
{{ render_field_with_errors(reset_password_form.password_confirm) }} {{ render_field(reset_password_form.confirm_password) }}
{{ render_field_errors(reset_password_form.csrf_token) }} {# {{ render_field_errors(reset_password_form.csrf_token) }}#}
{{ render_field(reset_password_form.submit) }} <button type="submit" class="btn btn-primary">Reset Password</button>
</form> </form>
{# {% include "security/_menu.html" %}#} {# {% include "security/_menu.html" %}#}
{% endblock content %} {% endblock content %}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from "macros.html" import render_selectable_table %} {% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}Tenant Selection{% endblock %} {% block title %}Tenant Selection{% endblock %}
@@ -8,37 +8,44 @@
{% block content %} {% block content %}
<form method="POST" action="{{ url_for('user_bp.handle_tenant_selection') }}"> <form method="POST" action="{{ url_for('user_bp.handle_tenant_selection') }}">
{{ render_selectable_table(headers=["Tenant ID", "Tenant Name", "Website"], rows=tenants, selectable=True, id="tenantsTable") }} {{ render_selectable_table(headers=["Tenant ID", "Tenant Name", "Website"], rows=rows, selectable=True, id="tenantsTable") }}
<div class="form-group mt-3"> <div class="form-group mt-3">
<button type="submit" name="action" value="select_tenant" class="btn btn-primary">Set Session Tenant</button> <button type="submit" name="action" value="select_tenant" class="btn btn-primary">Set Session Tenant</button>
<button type="submit" name="action" value="view_users" class="btn btn-secondary">View Users</button>
<button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button> <button type="submit" name="action" value="edit_tenant" class="btn btn-secondary">Edit Tenant</button>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block scripts %} {% block content_footer %}
<script> {{ render_pagination(pagination, 'user_bp.select_tenant') }}
$(document).ready(function() {
$('#tenantsTable').DataTable({
'columnDefs': [
{
'targets': 0,
'searchable': false,
'orderable': false,
'className': 'dt-body-center',
},
{
'targets': 1,
'orderable': true
},
{
'targets': 2,
'orderable': true
}
],
'order': [[1, 'asc']]
});
});
</script>
{% endblock %} {% endblock %}
{#{% block scripts %}#}
{#<script>#}
{#$(document).ready(function() {#}
{# $('#tenantsTable').DataTable({#}
{# 'columnDefs': [#}
{# {#}
{# 'targets': 0,#}
{# 'searchable': false,#}
{# 'orderable': false,#}
{# 'className': 'dt-body-center',#}
{# },#}
{# {#}
{# 'targets': 1,#}
{# 'orderable': true#}
{# },#}
{# {#}
{# 'targets': 2,#}
{# 'orderable': true#}
{# },#}
{# {#}
{# 'targets': 2,#}
{# 'orderable': true#}
{# },#}
{# ],#}
{# 'order': [[1, 'asc']]#}
{# });#}
{#});#}
{#</script>#}
{#{% endblock %}#}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% 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 %} {% block title %}View Tenant Domains{% endblock %}
@@ -8,38 +8,43 @@
{% block content %} {% block content %}
<form action="{{ url_for('user_bp.handle_tenant_domain_action') }}" method="POST"> <form action="{{ url_for('user_bp.handle_tenant_domain_action') }}" method="POST">
{{ render_selectable_table(headers=["ID", "Domain Name", "Valid To"], rows=tenant_domains, selectable=True, id="tenantDomainsTable") }} {{ render_selectable_table(headers=["ID", "Domain Name", "Valid To"], rows=rows, selectable=True, id="tenantDomainsTable") }}
<div class="form-group mt-3"> <div class="form-group mt-3">
<button type="submit" name="action" value="edit_tenant_domain" class="btn btn-primary">Edit Selected Domain</button> <button type="submit" name="action" value="edit_tenant_domain" class="btn btn-primary">Edit Selected Domain</button>
<!-- Additional buttons can be added here for other actions --> <!-- Additional buttons can be added here for other actions -->
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block scripts %}
<script> {% block content_footer %}
$(document).ready(function() { {{ render_pagination(pagination, 'user_bp.view_tenant_domains') }}
$('#tenantDomainsTable').DataTable({ {% endblock %}
'columnDefs': [
{ {% block scripts %}
'targets': 0, {#<script>#}
'searchable': false, {#$(document).ready(function() {#}
'orderable': false, {# $('#tenantDomainsTable').DataTable({#}
'className': 'dt-body-center', {# 'columnDefs': [#}
'render': function (data, type, full, meta) { {# {#}
return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">'; {# 'targets': 0,#}
} {# 'searchable': false,#}
}, {# 'orderable': false,#}
{ {# 'className': 'dt-body-center',#}
'targets': 1, {# 'render': function (data, type, full, meta) {#}
'orderable': true, {# return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';#}
}, {# }#}
{ {# },#}
'targets': 2, {# {#}
'orderable': true, {# 'targets': 1,#}
}, {# 'orderable': true,#}
], {# },#}
'order': [[1, 'asc']] {# {#}
}); {# 'targets': 2,#}
}); {# 'orderable': true,#}
</script> {# },#}
{# ],#}
{# 'order': [[1, 'asc']]#}
{# });#}
{#});#}
{#</script>#}
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from "macros.html" import render_selectable_table %} {% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Users{% endblock %} {% block title %}View Users{% endblock %}
@@ -8,42 +8,49 @@
{% block content %} {% block content %}
<form action="{{ url_for('user_bp.handle_user_action') }}" method="POST"> <form action="{{ url_for('user_bp.handle_user_action') }}" method="POST">
{{ render_selectable_table(headers=["User ID", "User Name", "Email"], rows=users, selectable=True, id="usersTable") }} {{ render_selectable_table(headers=["User ID", "User Name", "Email"], rows=rows, selectable=True, id="usersTable") }}
<div class="form-group mt-3"> <div class="form-group mt-3">
<button type="submit" name="action" value="edit_user" class="btn btn-primary">Edit Selected User</button> <button type="submit" name="action" value="edit_user" class="btn btn-primary">Edit Selected User</button>
<button type="submit" name="action" value="resend_confirmation_email" class="btn btn-secondary">Resend Confirmation Email</button>
<button type="submit" name="action" value="reset_uniquifier" class="btn btn-secondary">Reset Uniquifier</button>
<!-- Additional buttons can be added here for other actions --> <!-- Additional buttons can be added here for other actions -->
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block scripts %}
<script> {% block content_footer %}
$(document).ready(function() { {{ render_pagination(pagination, 'user_bp.select_tenant') }}
$('#usersTable').DataTable({ {% endblock %}
'columnDefs': [
{ {% block scripts %}
'targets': 0, {#<script>#}
'searchable': false, {#$(document).ready(function() {#}
'orderable': false, {# $('#usersTable').DataTable({#}
'className': 'dt-body-center', {# 'columnDefs': [#}
'render': function (data, type, full, meta) { {# {#}
return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">'; {# 'targets': 0,#}
} {# 'searchable': false,#}
}, {# 'orderable': false,#}
{ {# 'className': 'dt-body-center',#}
'targets': 1, {# 'render': function (data, type, full, meta) {#}
'orderable': true, {# return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';#}
}, {# }#}
{ {# },#}
'targets': 2, {# {#}
'orderable': true, {# 'targets': 1,#}
}, {# 'orderable': true,#}
{ {# },#}
'targets': 3, {# {#}
'orderable': true, {# 'targets': 2,#}
} {# 'orderable': true,#}
], {# },#}
'order': [[1, 'asc']] {# {#}
}); {# 'targets': 3,#}
}); {# 'orderable': true,#}
</script> {# }#}
{# ],#}
{# 'order': [[1, 'asc']]#}
{# });#}
{#});#}
{#</script>#}
{% endblock %} {% endblock %}

View File

@@ -22,6 +22,16 @@ def index():
return render_template('index.html') 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']) @basic_bp.route('/session_defaults', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def session_defaults(): def session_defaults():

View File

@@ -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')

View File

@@ -1,13 +1,20 @@
# views/security_views.py # 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 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 flask_security.forms import LoginForm
from urllib.parse import urlparse 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.models.user import User
from common.utils.nginx_utils import prefixed_url_for 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__) security_bp = Blueprint('security_bp', __name__)
@@ -15,11 +22,19 @@ security_bp = Blueprint('security_bp', __name__)
@security_bp.before_request @security_bp.before_request
def log_before_request(): def log_before_request():
current_app.logger.debug(f"Before request (security_bp): {request.method} {request.url}") 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 @security_bp.after_request
def log_after_request(response): def log_after_request(response):
current_app.logger.debug(f"After request (security_bp): {request.method} {request.url} - Status: {response.status}") 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 return response
@@ -34,11 +49,19 @@ def login():
current_app.logger.debug(f'Validating login form: {form.email.data}') current_app.logger.debug(f'Validating login form: {form.email.data}')
user = User.query.filter_by(email=form.email.data).first() user = User.query.filter_by(email=form.email.data).first()
if user is None or not verify_and_update_password(form.password.data, user): 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')) return redirect(prefixed_url_for('security_bp.login'))
login_user(user, remember=form.remember.data)
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')) 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) return render_template('security/login_user.html', login_user_form=form)
@@ -50,3 +73,76 @@ def logout():
logout_user() logout_user()
current_app.logger.debug('After Logout') current_app.logger.debug('After Logout')
return redirect(prefixed_url_for('basic_bp.index')) return redirect(prefixed_url_for('basic_bp.index'))
@security_bp.route('/confirm_email/<token>', 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/<token>', 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)

View File

@@ -48,7 +48,6 @@ class BaseUserForm(FlaskForm):
email = EmailField('Email', validators=[DataRequired(), Email()]) email = EmailField('Email', validators=[DataRequired(), Email()])
first_name = StringField('First Name', validators=[DataRequired(), Length(max=80)]) first_name = StringField('First Name', validators=[DataRequired(), Length(max=80)])
last_name = StringField('Last 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()]) valid_to = DateField('Valid to', id='form-control datepicker', validators=[Optional()])
tenant_id = IntegerField('Tenant ID', validators=[NumberRange(min=0)]) tenant_id = IntegerField('Tenant ID', validators=[NumberRange(min=0)])
roles = SelectMultipleField('Roles', coerce=int) roles = SelectMultipleField('Roles', coerce=int)
@@ -59,8 +58,6 @@ class BaseUserForm(FlaskForm):
class CreateUserForm(BaseUserForm): 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') submit = SubmitField('Create User')

View File

@@ -3,11 +3,13 @@ import uuid
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify 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 flask_security import hash_password, roles_required, roles_accepted, current_user
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
import ast import ast
from common.models.user import User, Tenant, Role, TenantDomain 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 .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm
from common.utils.database import Database from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro 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 @user_bp.before_request
def log_before_request(): def log_before_request():
current_app.logger.debug(f"Before request (user_bp): {request.method} {request.url}") 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 @user_bp.after_request
def log_after_request(response): def log_after_request(response):
current_app.logger.debug(f"After request (user_bp): {request.method} {request.url} - Status: {response.status}") 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 return response
@@ -133,12 +143,10 @@ def user():
password=hashed_password, password=hashed_password,
first_name=form.first_name.data, first_name=form.first_name.data,
last_name=form.last_name.data, last_name=form.last_name.data,
is_active=form.is_active.data,
valid_to=form.valid_to.data, valid_to=form.valid_to.data,
tenant_id=form.tenant_id.data tenant_id=form.tenant_id.data
) )
new_user.fs_uniquifier = str(uuid.uuid4())
timestamp = dt.now(tz.utc) timestamp = dt.now(tz.utc)
new_user.created_at = timestamp new_user.created_at = timestamp
new_user.updated_at = timestamp new_user.updated_at = timestamp
@@ -158,8 +166,11 @@ def user():
try: try:
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database') security.datastore.set_uniquifier()
flash('User added successfully.', 'success') 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')) return redirect(prefixed_url_for('user_bp.view_users'))
except Exception as e: except Exception as e:
current_app.logger.error(f'Failed to add user with name {new_user.user_name}. Error: {str(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 # Populate the user with form data
user.first_name = form.first_name.data user.first_name = form.first_name.data
user.last_name = form.last_name.data user.last_name = form.last_name.data
user.is_active = form.is_active.data
user.valid_to = form.valid_to.data user.valid_to = form.valid_to.data
user.updated_at = dt.now(tz.utc) user.updated_at = dt.now(tz.utc)
@@ -200,7 +210,8 @@ def edit_user(user_id):
db.session.commit() db.session.commit()
flash('User updated successfully.', 'success') flash('User updated successfully.', 'success')
return redirect( 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] form.roles.data = [role.id for role in user.roles]
return render_template('user/edit_user.html', form=form, user_id=user_id) 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') @user_bp.route('/select_tenant')
@roles_required('Super User') @roles_required('Super User')
def select_tenant(): 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']) @user_bp.route('/handle_tenant_selection', methods=['POST'])
@@ -239,17 +256,24 @@ def handle_tenant_selection():
return redirect(prefixed_url_for('select_tenant')) return redirect(prefixed_url_for('select_tenant'))
@user_bp.route('/view_users/<int:tenant_id>') @user_bp.route('/view_users')
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def view_users(tenant_id): def view_users():
tenant_id = int(tenant_id) page = request.args.get('page', 1, type=int)
users = User.query.filter_by(tenant_id=tenant_id).all() per_page = request.args.get('per_page', 10, type=int)
prepared_data = prepare_table_for_macro(users, [('id', ''), ('user_name', ''), ('email', '')]) tenant_id = session.get('tenant').get('id')
current_app.logger.debug(f'prepared_data: {prepared_data}') 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 # 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']) @user_bp.route('/handle_user_action', methods=['POST'])
@@ -257,25 +281,38 @@ def view_users(tenant_id):
def handle_user_action(): def handle_user_action():
user_identification = request.form['selected_row'] user_identification = request.form['selected_row']
user_id = ast.literal_eval(user_identification).get('value') user_id = ast.literal_eval(user_identification).get('value')
user = User.query.get_or_404(user_id)
action = request.form['action'] action = request.form['action']
if action == 'edit_user': if action == 'edit_user':
return redirect(prefixed_url_for('user_bp.edit_user', user_id=user_id)) return redirect(prefixed_url_for('user_bp.edit_user', user_id=user_id))
# Add more conditions for other actions elif action == 'resend_confirmation_email':
return redirect(prefixed_url_for('view_users')) 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/<int:tenant_id>') @user_bp.route('/view_tenant_domains')
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def view_tenant_domains(tenant_id): def view_tenant_domains():
tenant_id = int(tenant_id) page = request.args.get('page', 1, type=int)
tenant_domains = TenantDomain.query.filter_by(tenant_id=tenant_id).all() 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 # 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 # 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']) @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'for tenant {session["tenant"]["id"]}'
f'Error: {str(e)}') f'Error: {str(e)}')
return redirect( 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) 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']) @user_bp.route('/tenant_overview', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin') @roles_accepted('Super User', 'Tenant Admin')
def tenant_overview(): 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'] tenant_id = session['tenant']['id']
current_app.logger.debug(f'Generating overview for tenant {tenant_id}')
tenant = Tenant.query.get_or_404(tenant_id) tenant = Tenant.query.get_or_404(tenant_id)
form = TenantForm(obj=tenant) form = TenantForm(obj=tenant)
return render_template('user/tenant_overview.html', form=form) 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): def set_logging_information(obj, timestamp):
obj.created_at = timestamp obj.created_at = timestamp
obj.updated_at = timestamp obj.updated_at = timestamp

View File

@@ -20,6 +20,8 @@ def create_app(config_file=None):
else: else:
app.config.from_object(config_file) app.config.from_object(config_file)
app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_'
logging.config.dictConfig(LOGGING) logging.config.dictConfig(LOGGING)
register_extensions(app) register_extensions(app)

View File

@@ -11,3 +11,5 @@ langchain~=0.1.17
requests~=2.31.0 requests~=2.31.0
beautifulsoup4~=4.12.3 beautifulsoup4~=4.12.3
google~=3.0.0 google~=3.0.0
redis~=5.0.4
itsdangerous~=2.2.0