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

@@ -1,6 +1,6 @@
import logging
import os
from flask import Flask, render_template, jsonify
from flask import Flask, render_template, jsonify, flash, redirect, request
from flask_security import SQLAlchemyUserDatastore, LoginForm
from flask_security.signals import user_authenticated
from werkzeug.middleware.proxy_fix import ProxyFix
@@ -14,6 +14,7 @@ from common.utils.security import set_tenant_session_data
from .errors import register_error_handlers
from common.utils.celery_utils import make_celery, init_celery
from common.utils.debug_utils import log_request_middleware
from common.utils.nginx_utils import prefixed_url_for
def create_app(config_file=None):
@@ -27,6 +28,8 @@ def create_app(config_file=None):
else:
app.config.from_object(config_file)
app.config['SESSION_KEY_PREFIX'] = 'eveai_app_'
try:
os.makedirs(app.instance_path)
except OSError:
@@ -67,8 +70,9 @@ def create_app(config_file=None):
security_logger.setLevel(logging.DEBUG)
sqlalchemy_logger = logging.getLogger('sqlalchemy.engine')
sqlalchemy_logger.setLevel(logging.DEBUG)
# log_request_middleware(app) # Add this when debugging nginx or another proxy
log_request_middleware(app) # Add this when debugging nginx or another proxy
# Some generic Error Handling Routines
@app.errorhandler(Exception)
def handle_exception(e):
app.logger.error(f"Unhandled Exception: {e}", exc_info=True)

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">
<ul class="navbar-nav navbar-nav-hover mx-auto">
{% if current_user.is_authenticated %}
{{ dropdown('User Mgmt', 'contacts', [
{{ dropdown('Tenant Configuration', 'contacts', [
{'name': 'Tenant List', 'url': '/user/select_tenant', 'roles': ['Super User']},
{'name': 'Tenant Registration', 'url': '/user/tenant', 'roles': ['Super User']},
{'name': 'Generate Chat API Key', 'url': '/user/generate_chat_api_key', 'roles': ['Super User']},
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains/' + session['tenant']['id']|string, 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'Tenant Domain Registration', 'url': '/user/tenant_domain', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'User List', 'url': '/user/view_users/' + session['tenant']['id']|string, 'roles': ['Super User', 'Tenant Admin']},
{'name': 'User List', 'url': '/user/view_users', 'roles': ['Super User', 'Tenant Admin']},
{'name': 'User Registration', 'url': '/user/user', 'roles': ['Super User', 'Tenant Admin']},
]) }}
{% endif %}

View File

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

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table %}
{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}Tenant Selection{% endblock %}
@@ -8,37 +8,44 @@
{% block content %}
<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">
<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>
</div>
</form>
{% 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
}
],
'order': [[1, 'asc']]
});
});
</script>
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% 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' %}
{% from "macros.html" import render_selectable_table %}
{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Tenant Domains{% endblock %}
@@ -8,38 +8,43 @@
{% block content %}
<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">
<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 -->
</div>
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.view_tenant_domains') }}
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('#tenantDomainsTable').DataTable({
'columnDefs': [
{
'targets': 0,
'searchable': false,
'orderable': false,
'className': 'dt-body-center',
'render': function (data, type, full, meta) {
return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';
}
},
{
'targets': 1,
'orderable': true,
},
{
'targets': 2,
'orderable': true,
},
],
'order': [[1, 'asc']]
});
});
</script>
{#<script>#}
{#$(document).ready(function() {#}
{# $('#tenantDomainsTable').DataTable({#}
{# 'columnDefs': [#}
{# {#}
{# 'targets': 0,#}
{# 'searchable': false,#}
{# 'orderable': false,#}
{# 'className': 'dt-body-center',#}
{# 'render': function (data, type, full, meta) {#}
{# return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';#}
{# }#}
{# },#}
{# {#}
{# 'targets': 1,#}
{# 'orderable': true,#}
{# },#}
{# {#}
{# 'targets': 2,#}
{# 'orderable': true,#}
{# },#}
{# ],#}
{# 'order': [[1, 'asc']]#}
{# });#}
{#});#}
{#</script>#}
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% from "macros.html" import render_selectable_table %}
{% from "macros.html" import render_selectable_table, render_pagination %}
{% block title %}View Users{% endblock %}
@@ -8,42 +8,49 @@
{% block content %}
<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">
<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 -->
</div>
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('#usersTable').DataTable({
'columnDefs': [
{
'targets': 0,
'searchable': false,
'orderable': false,
'className': 'dt-body-center',
'render': function (data, type, full, meta) {
return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';
}
},
{
'targets': 1,
'orderable': true,
},
{
'targets': 2,
'orderable': true,
},
{
'targets': 3,
'orderable': true,
}
],
'order': [[1, 'asc']]
});
});
</script>
{#<script>#}
{#$(document).ready(function() {#}
{# $('#usersTable').DataTable({#}
{# 'columnDefs': [#}
{# {#}
{# 'targets': 0,#}
{# 'searchable': false,#}
{# 'orderable': false,#}
{# 'className': 'dt-body-center',#}
{# 'render': function (data, type, full, meta) {#}
{# return '<input type="radio" name="user_id" value="' + $('<div/>').text(data).html() + '">';#}
{# }#}
{# },#}
{# {#}
{# 'targets': 1,#}
{# 'orderable': true,#}
{# },#}
{# {#}
{# 'targets': 2,#}
{# 'orderable': true,#}
{# },#}
{# {#}
{# 'targets': 3,#}
{# 'orderable': true,#}
{# }#}
{# ],#}
{# 'order': [[1, 'asc']]#}
{# });#}
{#});#}
{#</script>#}
{% endblock %}

View File

@@ -22,6 +22,16 @@ def index():
return render_template('index.html')
@basic_bp.route('/confirm_email_ok', methods=['GET', ])
def confirm_email_ok():
return render_template('basic/confirm_email_ok.html')
@basic_bp.route('/confirm_email_fail', methods=['GET', ])
def confirm_email_fail():
return render_template('basic/confirm_email_fail.html')
@basic_bp.route('/session_defaults', methods=['GET', 'POST'])
@roles_accepted('Super User', 'Tenant Admin')
def session_defaults():

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
from flask import Blueprint, render_template, redirect, request, flash, current_app
from flask import Blueprint, render_template, redirect, request, flash, current_app, abort, session
from flask_security import current_user, login_required, login_user, logout_user
from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value
from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
from flask_security.forms import LoginForm
from urllib.parse import urlparse
import datetime as dt
from datetime import datetime as dt, timezone as tz
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
from common.models.user import User
from common.utils.nginx_utils import prefixed_url_for
from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, RequestResetForm
from common.extensions import db
from common.utils.security_utils import confirm_token, send_confirmation_email, send_reset_email
from common.utils.security import set_tenant_session_data
security_bp = Blueprint('security_bp', __name__)
@@ -15,11 +22,19 @@ security_bp = Blueprint('security_bp', __name__)
@security_bp.before_request
def log_before_request():
current_app.logger.debug(f"Before request (security_bp): {request.method} {request.url}")
if current_user and current_user.is_authenticated:
current_app.logger.debug(f"After request (security_bp): Current User: {current_user.email}")
else:
current_app.logger.debug(f"After request (security_bp): No user logged in")
@security_bp.after_request
def log_after_request(response):
current_app.logger.debug(f"After request (security_bp): {request.method} {request.url} - Status: {response.status}")
if current_user and current_user.is_authenticated:
current_app.logger.debug(f"After request (security_bp): Current User: {current_user.email}")
else:
current_app.logger.debug(f"After request (security_bp): No user logged in")
return response
@@ -34,11 +49,19 @@ def login():
current_app.logger.debug(f'Validating login form: {form.email.data}')
user = User.query.filter_by(email=form.email.data).first()
if user is None or not verify_and_update_password(form.password.data, user):
flash('Invalid username or password')
flash('Invalid username or password', 'danger')
return redirect(prefixed_url_for('security_bp.login'))
login_user(user, remember=form.remember.data)
return redirect(prefixed_url_for('user_bp.tenant_overview'))
if login_user(user):
current_app.logger.info(f'Login successful! Current User is {current_user.email}')
db.session.commit()
return redirect(prefixed_url_for('user_bp.tenant_overview'))
else:
flash('Invalid username or password', 'danger')
current_app.logger.debug(f'Failed to login user {user.email}')
abort(401)
else:
current_app.logger.debug(f'Invalid login form: {form.errors}')
return render_template('security/login_user.html', login_user_form=form)
@@ -50,3 +73,76 @@ def logout():
logout_user()
current_app.logger.debug('After Logout')
return redirect(prefixed_url_for('basic_bp.index'))
@security_bp.route('/confirm_email/<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()])
first_name = StringField('First Name', validators=[DataRequired(), Length(max=80)])
last_name = StringField('Last Name', validators=[DataRequired(), Length(max=80)])
is_active = BooleanField('Is Active', id='flexSwitchCheckDefault', default=True)
valid_to = DateField('Valid to', id='form-control datepicker', validators=[Optional()])
tenant_id = IntegerField('Tenant ID', validators=[NumberRange(min=0)])
roles = SelectMultipleField('Roles', coerce=int)
@@ -59,8 +58,6 @@ class BaseUserForm(FlaskForm):
class CreateUserForm(BaseUserForm):
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), Length(min=8)])
submit = SubmitField('Create User')

View File

@@ -3,11 +3,13 @@ import uuid
from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
from flask_security import hash_password, roles_required, roles_accepted, current_user
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
import ast
from common.models.user import User, Tenant, Role, TenantDomain
from common.extensions import db, kms_client
from common.extensions import db, kms_client, security
from common.utils.security_utils import send_confirmation_email, send_reset_email
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm
from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro
@@ -20,11 +22,19 @@ user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@user_bp.before_request
def log_before_request():
current_app.logger.debug(f"Before request (user_bp): {request.method} {request.url}")
if current_user and current_user.is_authenticated:
current_app.logger.debug(f"After request (user_bp): Current User: {current_user.email}")
else:
current_app.logger.debug(f"After request (user_bp): No user logged in")
@user_bp.after_request
def log_after_request(response):
current_app.logger.debug(f"After request (user_bp): {request.method} {request.url} - Status: {response.status}")
if current_user and current_user.is_authenticated:
current_app.logger.debug(f"After request (user_bp): Current User: {current_user.email}")
else:
current_app.logger.debug(f"After request (user_bp): No user logged in")
return response
@@ -119,7 +129,7 @@ def edit_tenant(tenant_id):
@roles_accepted('Super User', 'Tenant Admin')
def user():
form = CreateUserForm()
form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant
form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant
if form.validate_on_submit():
current_app.logger.info(f"Adding User for tenant {session['tenant']['id']} ")
if form.password.data != form.confirm_password.data:
@@ -133,12 +143,10 @@ def user():
password=hashed_password,
first_name=form.first_name.data,
last_name=form.last_name.data,
is_active=form.is_active.data,
valid_to=form.valid_to.data,
tenant_id=form.tenant_id.data
)
new_user.fs_uniquifier = str(uuid.uuid4())
timestamp = dt.now(tz.utc)
new_user.created_at = timestamp
new_user.updated_at = timestamp
@@ -158,8 +166,11 @@ def user():
try:
db.session.add(new_user)
db.session.commit()
current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database')
flash('User added successfully.', 'success')
security.datastore.set_uniquifier()
send_confirmation_email(new_user)
current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database'
f'Confirmation email sent to {new_user.email}')
flash('User added successfully and confirmation email sent.', 'success')
return redirect(prefixed_url_for('user_bp.view_users'))
except Exception as e:
current_app.logger.error(f'Failed to add user with name {new_user.user_name}. Error: {str(e)}')
@@ -179,7 +190,6 @@ def edit_user(user_id):
# Populate the user with form data
user.first_name = form.first_name.data
user.last_name = form.last_name.data
user.is_active = form.is_active.data
user.valid_to = form.valid_to.data
user.updated_at = dt.now(tz.utc)
@@ -200,7 +210,8 @@ def edit_user(user_id):
db.session.commit()
flash('User updated successfully.', 'success')
return redirect(
prefixed_url_for('user_bp.edit_user', user_id=user.id)) # Assuming there's a user profile view to redirect to
prefixed_url_for('user_bp.edit_user',
user_id=user.id)) # Assuming there's a user profile view to redirect to
form.roles.data = [role.id for role in user.roles]
return render_template('user/edit_user.html', form=form, user_id=user_id)
@@ -209,11 +220,17 @@ def edit_user(user_id):
@user_bp.route('/select_tenant')
@roles_required('Super User')
def select_tenant():
tenants = Tenant.query.all() # Fetch all tenants from the database
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
prepared_data = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', '')])
query = Tenant.query.order_by(Tenant.name) # Fetch all tenants from the database
return render_template('user/select_tenant.html', tenants=prepared_data)
pagination = query.paginate(page=page, per_page=per_page)
tenants = pagination.items
rows = prepare_table_for_macro(tenants, [('id', ''), ('name', ''), ('website', '')])
return render_template('user/select_tenant.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_tenant_selection', methods=['POST'])
@@ -239,17 +256,24 @@ def handle_tenant_selection():
return redirect(prefixed_url_for('select_tenant'))
@user_bp.route('/view_users/<int:tenant_id>')
@user_bp.route('/view_users')
@roles_accepted('Super User', 'Tenant Admin')
def view_users(tenant_id):
tenant_id = int(tenant_id)
users = User.query.filter_by(tenant_id=tenant_id).all()
def view_users():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
prepared_data = prepare_table_for_macro(users, [('id', ''), ('user_name', ''), ('email', '')])
current_app.logger.debug(f'prepared_data: {prepared_data}')
tenant_id = session.get('tenant').get('id')
query = User.query.filter_by(tenant_id=tenant_id).order_by(User.user_name)
pagination = query.paginate(page=page, per_page=per_page)
users = pagination.items
# prepare table data
rows = prepare_table_for_macro(users, [('id', ''), ('user_name', ''), ('email', '')])
# Render the users in a template
return render_template('user/view_users.html', users=prepared_data)
return render_template('user/view_users.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_user_action', methods=['POST'])
@@ -257,25 +281,38 @@ def view_users(tenant_id):
def handle_user_action():
user_identification = request.form['selected_row']
user_id = ast.literal_eval(user_identification).get('value')
user = User.query.get_or_404(user_id)
action = request.form['action']
if action == 'edit_user':
return redirect(prefixed_url_for('user_bp.edit_user', user_id=user_id))
# Add more conditions for other actions
return redirect(prefixed_url_for('view_users'))
elif action == 'resend_confirmation_email':
send_confirmation_email(user)
flash(f'Confirmation email sent to {user.email}.', 'success')
elif action == 'reset_uniquifier':
reset_uniquifier(user)
flash(f'Uniquifier reset for {user.user_name}.', 'success')
return redirect(prefixed_url_for('user_bp.view_users'))
@user_bp.route('/view_tenant_domains/<int:tenant_id>')
@user_bp.route('/view_tenant_domains')
@roles_accepted('Super User', 'Tenant Admin')
def view_tenant_domains(tenant_id):
tenant_id = int(tenant_id)
tenant_domains = TenantDomain.query.filter_by(tenant_id=tenant_id).all()
def view_tenant_domains():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
tenant_id = session.get('tenant').get('id')
query = TenantDomain.query.filter_by(tenant_id=tenant_id).order_by(TenantDomain.domain)
pagination = query.paginate(page=page, per_page=per_page)
tenant_domains = pagination.items
# prepare table data
prepared_data = prepare_table_for_macro(tenant_domains, [('id', ''), ('domain', ''), ('valid_to', '')])
rows = prepare_table_for_macro(tenant_domains, [('id', ''), ('domain', ''), ('valid_to', '')])
# Render the users in a template
return render_template('user/view_tenant_domains.html', tenant_domains=prepared_data)
return render_template('user/view_tenant_domains.html', rows=rows, pagination=pagination)
@user_bp.route('/handle_tenant_domain_action', methods=['POST'])
@@ -336,7 +373,8 @@ def edit_tenant_domain(tenant_domain_id):
f'for tenant {session["tenant"]["id"]}'
f'Error: {str(e)}')
return redirect(
prefixed_url_for('user_bp.view_tenant_domains', tenant_id=session['tenant']['id'])) # Assuming there's a user profile view to redirect to
prefixed_url_for('user_bp.view_tenant_domains',
tenant_id=session['tenant']['id'])) # Assuming there's a user profile view to redirect to
return render_template('user/edit_tenant_domain.html', form=form, tenant_domain_id=tenant_domain_id)
@@ -374,12 +412,23 @@ def generate_chat_api_key():
@user_bp.route('/tenant_overview', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def tenant_overview():
current_app.logger.debug('Rendering tenant overview')
current_app.logger.debug(f'current_user: {current_user}')
current_app.logger.debug(f'Current user roles: {current_user.roles}')
tenant_id = session['tenant']['id']
current_app.logger.debug(f'Generating overview for tenant {tenant_id}')
tenant = Tenant.query.get_or_404(tenant_id)
form = TenantForm(obj=tenant)
return render_template('user/tenant_overview.html', form=form)
def reset_uniquifier(user):
security.datastore.set_uniquifier(user)
db.session.add(user)
db.session.commit()
send_reset_email(user)
def set_logging_information(obj, timestamp):
obj.created_at = timestamp
obj.updated_at = timestamp