Optimizing admin interface for user domain, completing security views
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
0
common/utils/model_utils.py
Normal file
0
common/utils/model_utils.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
53
common/utils/security_utils.py
Normal file
53
common/utils/security_utils.py
Normal 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)
|
||||||
|
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
16
eveai_app/templates/basic/confirm_email_fail.html
Normal file
16
eveai_app/templates/basic/confirm_email_fail.html
Normal 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 %}
|
||||||
16
eveai_app/templates/basic/confirm_email_ok.html
Normal file
16
eveai_app/templates/basic/confirm_email_ok.html
Normal 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 %}
|
||||||
13
eveai_app/templates/email/activate.html
Normal file
13
eveai_app/templates/email/activate.html
Normal 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>
|
||||||
13
eveai_app/templates/email/reset_password.html
Normal file
13
eveai_app/templates/email/reset_password.html
Normal 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>
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}#}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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():
|
||||||
|
|||||||
21
eveai_app/views/security_forms.py
Normal file
21
eveai_app/views/security_forms.py
Normal 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')
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user