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

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