- Modernized authentication with the introduction of TenantProject
- Created a base mail template - Adapt and improve document API to usage of catalogs and processors - Adapt eveai_sync to new authentication mechanism and usage of catalogs and processors
This commit is contained in:
@@ -2,15 +2,18 @@
|
||||
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_mailman import EmailMessage
|
||||
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.models.user import User, Tenant, Role, TenantDomain, TenantProject
|
||||
from common.extensions import db, security, minio_client, simple_encryption
|
||||
from common.utils.security_utils import send_confirmation_email, send_reset_email
|
||||
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm
|
||||
from config.type_defs.service_types import SERVICE_TYPES
|
||||
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
|
||||
TenantProjectForm, EditTenantProjectForm
|
||||
from common.utils.database import Database
|
||||
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
|
||||
from common.utils.simple_encryption import generate_api_key
|
||||
@@ -21,7 +24,7 @@ user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
||||
|
||||
@user_bp.before_request
|
||||
def log_before_request():
|
||||
pass
|
||||
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
||||
|
||||
|
||||
@user_bp.after_request
|
||||
@@ -33,7 +36,9 @@ def log_after_request(response):
|
||||
@roles_required('Super User')
|
||||
def tenant():
|
||||
form = TenantForm()
|
||||
current_app.logger.debug(f'Tenant form: {form}')
|
||||
if form.validate_on_submit():
|
||||
current_app.logger.debug(f'Tenant form submitted: {form.data}')
|
||||
# Handle the required attributes
|
||||
new_tenant = Tenant()
|
||||
form.populate_obj(new_tenant)
|
||||
@@ -53,7 +58,7 @@ def tenant():
|
||||
return render_template('user/tenant.html', form=form)
|
||||
|
||||
current_app.logger.info(f"Successfully created tenant {new_tenant.id} in Database")
|
||||
flash(f"Successfully created tenant {new_tenant.id} in Database")
|
||||
flash(f"Successfully created tenant {new_tenant.id} in Database", 'success')
|
||||
|
||||
# Create schema for new tenant
|
||||
current_app.logger.info(f"Creating schema for tenant {new_tenant.id}")
|
||||
@@ -442,6 +447,163 @@ def tenant_overview():
|
||||
return render_template('user/tenant_overview.html', form=form)
|
||||
|
||||
|
||||
@user_bp.route('/tenant_project', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def tenant_project():
|
||||
form = TenantProjectForm()
|
||||
if request.method == 'GET':
|
||||
# Initialize the API key
|
||||
new_api_key = generate_api_key(prefix="EveAI")
|
||||
form.unencrypted_api_key.data = new_api_key
|
||||
form.visual_api_key.data = f"EVEAI-...{new_api_key[-4:]}"
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_tenant_project = TenantProject()
|
||||
form.populate_obj(new_tenant_project)
|
||||
new_tenant_project.tenant_id = session['tenant']['id']
|
||||
new_tenant_project.encrypted_api_key = simple_encryption.encrypt_api_key(new_tenant_project.unencrypted_api_key)
|
||||
set_logging_information(new_tenant_project, dt.now(tz.utc))
|
||||
|
||||
# Add new Tenant Project to the database
|
||||
try:
|
||||
db.session.add(new_tenant_project)
|
||||
db.session.commit()
|
||||
|
||||
# Send email notification
|
||||
services = [SERVICE_TYPES[service]['name']
|
||||
for service in form.services.data
|
||||
if service in SERVICE_TYPES]
|
||||
|
||||
email_sent = send_api_key_notification(
|
||||
tenant_id=session['tenant']['id'],
|
||||
tenant_name=session['tenant']['name'],
|
||||
project_name=new_tenant_project.name,
|
||||
api_key=new_tenant_project.unencrypted_api_key,
|
||||
services=services,
|
||||
responsible_email=form.responsible_email.data
|
||||
)
|
||||
|
||||
if email_sent:
|
||||
flash('Tenant Project created successfully and notification email sent.', 'success')
|
||||
else:
|
||||
flash('Tenant Project created successfully but failed to send notification email.', 'warning')
|
||||
|
||||
current_app.logger.info(f'Tenant Project {new_tenant_project.name} added for tenant '
|
||||
f'{session['tenant']['id']}.')
|
||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to create Tenant Project. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f"Failed to create Tenant Project for tenant {session['tenant']['id']}. "
|
||||
f"Error: {str(e)}")
|
||||
|
||||
return render_template('user/tenant_project.html', form=form)
|
||||
|
||||
|
||||
@user_bp.route('/tenant_projects', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def tenant_projects():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
tenant_id = session['tenant']['id']
|
||||
query = TenantProject.query.filter_by(tenant_id=tenant_id).order_by(TenantProject.id)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
the_tenant_projects = pagination.items
|
||||
|
||||
# prepare table data
|
||||
rows = prepare_table_for_macro(the_tenant_projects, [('id', ''), ('name', ''), ('visual_api_key', ''),
|
||||
('responsible_email', ''), ('active', '')])
|
||||
|
||||
# Render the catalogs in a template
|
||||
return render_template('user/tenant_projects.html', rows=rows, pagination=pagination)
|
||||
|
||||
|
||||
@user_bp.route('/handle_tenant_project_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def handle_tenant_project_selection():
|
||||
tenant_project_identification = request.form.get('selected_row')
|
||||
tenant_project_id = ast.literal_eval(tenant_project_identification).get('value')
|
||||
action = request.form.get('action')
|
||||
tenant_project = TenantProject.query.get_or_404(tenant_project_id)
|
||||
|
||||
if action == 'edit_tenant_project':
|
||||
return redirect(prefixed_url_for('user_bp.edit_tenant_project', tenant_project_id=tenant_project_id))
|
||||
elif action == 'invalidate_tenant_project':
|
||||
tenant_project.active = False
|
||||
try:
|
||||
db.session.add(tenant_project)
|
||||
db.session.commit()
|
||||
flash('Tenant Project invalidated successfully.', 'success')
|
||||
current_app.logger.info(f'Tenant Project {tenant_project.name} invalidated for tenant '
|
||||
f'{session['tenant']['id']}.')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to invalidate Tenant Project {tenant_project.name}. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f"Failed to invalidate Tenant Project for tenant {session['tenant']['id']}. "
|
||||
f"Error: {str(e)}")
|
||||
elif action == 'delete_tenant_project':
|
||||
return redirect(prefixed_url_for('user_bp.delete_tenant_project', tenant_project_id=tenant_project_id))
|
||||
|
||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||
|
||||
|
||||
@user_bp.route('/tenant_project/<int:tenant_project_id>', methods=['GET','POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def edit_tenant_project(tenant_project_id):
|
||||
tenant_project = TenantProject.query.get_or_404(tenant_project_id)
|
||||
tenant_id = session['tenant']['id']
|
||||
|
||||
form = EditTenantProjectForm(obj=tenant_project)
|
||||
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(tenant_project)
|
||||
update_logging_information(tenant_project, dt.now(tz.utc))
|
||||
|
||||
try:
|
||||
db.session.add(tenant_project)
|
||||
db.session.commit()
|
||||
flash('Tenant Project updated successfully.', 'success')
|
||||
current_app.logger.info(f'Tenant Project {tenant_project.name} updated for tenant {tenant_id}.')
|
||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update Tenant Project. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f"Failed to update Tenant Project {tenant_project.name} for tenant {tenant_id}. ")
|
||||
|
||||
return render_template('user/edit_tenant.html', form=form, tenant_project_id=tenant_project_id)
|
||||
|
||||
|
||||
@user_bp.route('/tenant_project/delete/<int:tenant_project_id>', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def delete_tenant_project(tenant_project_id):
|
||||
tenant_id = session['tenant']['id']
|
||||
tenant_project = TenantProject.query.get_or_404(tenant_project_id)
|
||||
|
||||
# Ensure project belongs to current tenant
|
||||
if tenant_project.tenant_id != tenant_id:
|
||||
flash('You do not have permission to delete this project.', 'danger')
|
||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('user/confirm_delete_tenant_project.html',
|
||||
tenant_project=tenant_project)
|
||||
|
||||
try:
|
||||
project_name = tenant_project.name
|
||||
db.session.delete(tenant_project)
|
||||
db.session.commit()
|
||||
flash(f'Tenant Project "{project_name}" successfully deleted.', 'success')
|
||||
current_app.logger.info(f'Tenant Project {project_name} deleted for tenant {tenant_id}')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to delete Tenant Project. Error: {str(e)}', 'danger')
|
||||
current_app.logger.error(f'Failed to delete Tenant Project {tenant_project_id}. Error: {str(e)}')
|
||||
|
||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||
|
||||
|
||||
def reset_uniquifier(user):
|
||||
security.datastore.set_uniquifier(user)
|
||||
db.session.add(user)
|
||||
@@ -459,3 +621,64 @@ def set_logging_information(obj, timestamp):
|
||||
def update_logging_information(obj, timestamp):
|
||||
obj.updated_at = timestamp
|
||||
obj.updated_by = current_user.id
|
||||
|
||||
|
||||
def get_notification_email(tenant_id, user_email=None):
|
||||
"""
|
||||
Determine which email address to use for notification.
|
||||
Priority: Provided email > Primary contact > Default email
|
||||
"""
|
||||
if user_email:
|
||||
return user_email
|
||||
|
||||
# Try to find primary contact
|
||||
primary_contact = User.query.filter_by(
|
||||
tenant_id=tenant_id,
|
||||
is_primary_contact=True
|
||||
).first()
|
||||
|
||||
if primary_contact:
|
||||
return primary_contact.email
|
||||
|
||||
return "pieter@askeveai.com"
|
||||
|
||||
|
||||
def send_api_key_notification(tenant_id, tenant_name, project_name, api_key, services, responsible_email=None):
|
||||
"""
|
||||
Send API key notification email
|
||||
"""
|
||||
recipient_email = get_notification_email(tenant_id, responsible_email)
|
||||
|
||||
# Prepare email content
|
||||
context = {
|
||||
'tenant_id': tenant_id,
|
||||
'tenant_name': tenant_name,
|
||||
'project_name': project_name,
|
||||
'api_key': api_key,
|
||||
'services': services,
|
||||
'year': dt.now(tz.utc).year,
|
||||
'promo_image_url': current_app.config.get('PROMOTIONAL_IMAGE_URL',
|
||||
'https://static.askeveai.com/promo/default.jpg')
|
||||
}
|
||||
|
||||
try:
|
||||
# Create email message
|
||||
msg = EmailMessage(
|
||||
subject='Your new API-key from Ask Eve AI (Evie)',
|
||||
body=render_template('email/api_key_notification.html', **context),
|
||||
from_email=current_app.config['MAIL_DEFAULT_SENDER'],
|
||||
to=[recipient_email]
|
||||
)
|
||||
|
||||
# Set HTML content type
|
||||
msg.content_subtype = "html"
|
||||
|
||||
# Send email
|
||||
msg.send()
|
||||
|
||||
current_app.logger.info(f"API key notification sent to {recipient_email} for tenant {tenant_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to send API key notification email: {str(e)}")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user