- 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:
@@ -10,7 +10,7 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for('document_bp.handle_catalog_selection') }}">
|
||||
{{ render_selectable_table(headers=["Catalog ID", "Name"], rows=rows, selectable=True, id="catalogsTable") }}
|
||||
{{ render_selectable_table(headers=["Catalog ID", "Name", "Type"], rows=rows, selectable=True, id="catalogsTable") }}
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="set_session_catalog" class="btn btn-primary">Set Session Catalog</button>
|
||||
<button type="submit" name="action" value="edit_catalog" class="btn btn-primary">Edit Catalog</button>
|
||||
|
||||
28
eveai_app/templates/email/api_key_notification.html
Normal file
28
eveai_app/templates/email/api_key_notification.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "email/base.html" %}
|
||||
{% block content %}
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>A new API project has been created for your Ask Eve AI tenant. Here are the details:</p>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>Tenant ID:</strong> {{ tenant_id }}</p>
|
||||
<p><strong>Tenant Name:</strong> {{ tenant_name }}</p>
|
||||
<p><strong>Project Name:</strong> {{ project_name }}</p>
|
||||
<p><strong>API Key:</strong> <span style="font-family: monospace; background-color: #f0f0f0; padding: 5px;">{{ api_key }}</span></p>
|
||||
|
||||
<div style="margin-top: 15px;">
|
||||
<p><strong>Enabled Services:</strong></p>
|
||||
<ul style="list-style-type: none; padding-left: 0;">
|
||||
{% for service in services %}
|
||||
<li>✓ {{ service }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<strong>Important:</strong> Please store this API key securely. It cannot be retrieved once this email is gone.
|
||||
</div>
|
||||
|
||||
<p>You can start using this API key right away to interact with our services. For documentation and usage examples, please visit our <a href="https://docs.askeveai.com">documentation</a>.</p>
|
||||
{% endblock %}
|
||||
106
eveai_app/templates/email/base.html
Normal file
106
eveai_app/templates/email/base.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ subject|default('Message from Ask Eve AI') }}</title>
|
||||
<style>
|
||||
.email-container {
|
||||
font-family: Tahoma, Geneva, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.header img {
|
||||
max-width: 200px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.signature {
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.footer img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
width: 600px; /* Match the container width */
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.footer img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.social-links {
|
||||
margin: 20px 0;
|
||||
}
|
||||
.social-links a {
|
||||
margin: 0 10px;
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
.info-box {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #0066cc;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.warning-box {
|
||||
background-color: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<img src="https://askeveai.com/wp-content/uploads/2024/07/Logo-Square-small.png" alt="Ask Eve AI Logo">
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="signature">
|
||||
Best regards,<br>
|
||||
Evie
|
||||
</div>
|
||||
|
||||
{% if promo_image_url %}
|
||||
<a href="https://www.askeveai.com">
|
||||
<img src="{{ promo_image_url }}" alt="Ask Eve AI Promotion">
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="social-links">
|
||||
<a href="https://twitter.com/askeveai">Twitter</a>
|
||||
<a href="https://linkedin.com/company/ask-eve-ai">LinkedIn</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-text">
|
||||
© {{ year }} Ask Eve AI. All rights reserved.<br>
|
||||
<a href="https://www.askeveai.com/privacy">Privacy Policy</a> |
|
||||
<a href="https://www.askeveai.com/terms">Terms of Service</a>
|
||||
{% if unsubscribe_url %}
|
||||
| <a href="{{ unsubscribe_url }}">Unsubscribe</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
28
eveai_app/templates/entitlements/view_licenses.html
Normal file
28
eveai_app/templates/entitlements/view_licenses.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}View Licenses{% endblock %}
|
||||
|
||||
{% block content_title %}View Licenses{% endblock %}
|
||||
{% block content_description %}View Licenses{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for('entitlements_bp.handle_license_selection') }}" method="POST">
|
||||
{{ render_selectable_table(headers=["License ID", "Name", "Start Date", "End Date", "Active"], rows=rows, selectable=True, id="licensesTable") }}
|
||||
<!-- <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="send_password_reset_email" class="btn btn-secondary">Send Password Reset 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, 'entitlements_bp.view_licenses') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,7 @@
|
||||
{% block content_description %}View License Usage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url_for('user_bp.handle_user_action') }}" method="POST">
|
||||
<form action="{{ url_for('entitlements_bp.handle_usage_selection') }}" method="POST">
|
||||
{{ render_selectable_table(headers=["Usage ID", "Start Date", "End Date", "Storage (MiB)", "Embedding (MiB)", "Interaction (tokens)"], rows=rows, selectable=False, id="usagesTable") }}
|
||||
<!-- <div class="form-group mt-3">-->
|
||||
<!-- <button type="submit" name="action" value="edit_user" class="btn btn-primary">Edit Selected User</button>-->
|
||||
@@ -20,7 +20,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'user_bp.select_tenant') }}
|
||||
{{ render_pagination(pagination, 'entitlements_bp.view_usages') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
@@ -1,86 +1,3 @@
|
||||
<!--{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %}-->
|
||||
<!-- {% set disabled = field.name in disabled_fields %}-->
|
||||
<!-- {% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}-->
|
||||
<!-- {% if field.name not in exclude_fields %}-->
|
||||
<!-- {% if field.type == 'BooleanField' %}-->
|
||||
<!-- <div class="form-check">-->
|
||||
<!-- {{ field(class="form-check-input " + class, type="checkbox", id="flexSwitchCheckDefault") }}-->
|
||||
<!-- {{ field.label(class="form-check-label", for="flexSwitchCheckDefault", disabled=disabled) }}-->
|
||||
<!-- </div>-->
|
||||
<!-- {% else %}-->
|
||||
<!-- <div class="form-group">-->
|
||||
<!-- {{ field.label(class="form-label") }}-->
|
||||
<!-- {{ field(class="form-control " + class, disabled=disabled) }}-->
|
||||
<!-- {% if field.errors %}-->
|
||||
<!-- <div class="invalid-feedback">-->
|
||||
<!-- {% for error in field.errors %}-->
|
||||
<!-- {{ error }}-->
|
||||
<!-- {% endfor %}-->
|
||||
<!-- </div>-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- </div>-->
|
||||
<!-- {% endif %}-->
|
||||
<!-- {% endif %}-->
|
||||
<!--{% endmacro %}-->
|
||||
|
||||
{% macro render_field_old(field, disabled_fields=[], exclude_fields=[], class='') %}
|
||||
<!-- Debug info -->
|
||||
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
|
||||
|
||||
{% set disabled = field.name in disabled_fields %}
|
||||
{% set exclude_fields = exclude_fields + ['csrf_token', 'submit'] %}
|
||||
{% if field.name not in exclude_fields %}
|
||||
{% if field.type == 'BooleanField' %}
|
||||
<div class="form-group">
|
||||
<div class="form-check form-switch">
|
||||
{{ field(class="form-check-input " + class, disabled=disabled) }}
|
||||
{% if field.description %}
|
||||
{{ field.label(class="form-check-label",
|
||||
**{'data-bs-toggle': 'tooltip',
|
||||
'data-bs-placement': 'right',
|
||||
'title': field.description}) }}
|
||||
{% else %}
|
||||
{{ field.label(class="form-check-label") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
{% if field.description %}
|
||||
{{ field.label(class="form-label",
|
||||
**{'data-bs-toggle': 'tooltip',
|
||||
'data-bs-placement': 'right',
|
||||
'title': field.description}) }}
|
||||
{% else %}
|
||||
{{ field.label(class="form-label") }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
|
||||
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
|
||||
{{ field(class="form-control d-none " + class, disabled=disabled) }}
|
||||
{% else %}
|
||||
{{ field(class="form-control " + class, disabled=disabled) }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_field(field, disabled_fields=[], exclude_fields=[], class='') %}
|
||||
<!-- Debug info -->
|
||||
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
|
||||
@@ -97,8 +14,20 @@
|
||||
**{'data-bs-toggle': 'tooltip',
|
||||
'data-bs-placement': 'right',
|
||||
'title': field.description}) }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ field.label(class="form-check-label") }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if field.errors %}
|
||||
@@ -116,8 +45,20 @@
|
||||
**{'data-bs-toggle': 'tooltip',
|
||||
'data-bs-placement': 'right',
|
||||
'title': field.description}) }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ field.label(class="form-label") }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
|
||||
@@ -147,14 +88,67 @@
|
||||
{% if field.type == 'BooleanField' %}
|
||||
<div class="form-check">
|
||||
{{ field(class="form-check-input", type="checkbox", id="flexSwitchCheckDefault") }}
|
||||
{{ field.label(class="form-check-label", for="flexSwitchCheckDefault", disabled=disabled) }}
|
||||
{% if field.description %}
|
||||
{{ field.label(class="form-check-label",
|
||||
for="flexSwitchCheckDefault",
|
||||
disabled=disabled,
|
||||
**{'data-bs-toggle': 'tooltip',
|
||||
'data-bs-placement': 'right',
|
||||
'title': field.description}) }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ field.label(class="form-check-label", for="flexSwitchCheckDefault", disabled=disabled) }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
{{ field.label(class="form-label") }}
|
||||
{{ field(class="form-control", disabled=disabled) }}
|
||||
{% if field.description %}
|
||||
<div class="field-label-wrapper">
|
||||
{{ field.label(class="form-label",
|
||||
**{'data-bs-toggle': 'tooltip',
|
||||
'data-bs-placement': 'right',
|
||||
'title': field.description}) }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="field-label-wrapper">
|
||||
{{ field.label(class="form-label") }}
|
||||
{% if field.flags.required %}
|
||||
<span class="required-field-indicator" aria-hidden="true">
|
||||
<i class="material-symbols-outlined required-icon">check_circle</i>
|
||||
</span>
|
||||
<span class="visually-hidden">Required field</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.type == 'TextAreaField' and 'json-editor' in field.render_kw.get('class', '') %}
|
||||
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
|
||||
{{ field(class="form-control d-none", disabled=disabled) }}
|
||||
{% elif field.type == 'SelectField' %}
|
||||
{{ field(class="form-control form-select", disabled=disabled) }}
|
||||
{% else %}
|
||||
{{ field(class="form-control", disabled=disabled) }}
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div class="invalid-feedback">
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
{'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), '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 Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Tenant Admin']},
|
||||
{'name': 'Tenant Project Registration', 'url': '/user/tenant_project', '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']},
|
||||
]) }}
|
||||
@@ -107,6 +109,7 @@
|
||||
{'name': 'License Tier Registration', 'url': '/entitlements/license_tier', 'roles': ['Super User']},
|
||||
{'name': 'All License Tiers', 'url': '/entitlements/view_license_tiers', 'roles': ['Super User']},
|
||||
{'name': 'Trigger Actions', 'url': '/administration/trigger_actions', 'roles': ['Super User']},
|
||||
{'name': 'All Licenses', 'url': 'entitlements/view_licenses', 'roles': ['Super User', 'Tenant Admin']},
|
||||
{'name': 'Usage', 'url': '/entitlements/view_usages', 'roles': ['Super User', 'Tenant Admin']},
|
||||
]) }}
|
||||
{% endif %}
|
||||
@@ -122,17 +125,6 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="navbar-nav d-lg-block d-none">
|
||||
<li class="nav-item">
|
||||
<a href="/document/catalogs" class="btn btn-sm bg-gradient-primary mb-0 me-2">
|
||||
{% if 'catalog_name' in session %}
|
||||
CATALOG: {{ session['catalog_name'] }}
|
||||
{% else %}
|
||||
CHOOSE CATALOG
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav d-lg-block d-none">
|
||||
<li class="nav-item">
|
||||
<a href="/session_defaults" class="btn btn-sm bg-gradient-primary mb-0">
|
||||
|
||||
@@ -59,6 +59,78 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get all forms with tabs
|
||||
const formsWithTabs = document.querySelectorAll('form');
|
||||
|
||||
formsWithTabs.forEach(form => {
|
||||
// Handle the form's submit event
|
||||
form.addEventListener('submit', function(event) {
|
||||
const invalidFields = form.querySelectorAll(':invalid');
|
||||
|
||||
if (invalidFields.length > 0) {
|
||||
// Prevent form submission
|
||||
event.preventDefault();
|
||||
|
||||
// Find which tab contains the first invalid field
|
||||
const firstInvalidField = invalidFields[0];
|
||||
const tabPane = firstInvalidField.closest('.tab-pane');
|
||||
|
||||
if (tabPane) {
|
||||
// Get the tab ID
|
||||
const tabId = tabPane.id;
|
||||
|
||||
// Find and click the corresponding tab button
|
||||
const tabButton = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="#${tabId}"]`);
|
||||
if (tabButton) {
|
||||
const tab = new bootstrap.Tab(tabButton);
|
||||
tab.show();
|
||||
}
|
||||
|
||||
// Scroll the invalid field into view and focus it
|
||||
firstInvalidField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
firstInvalidField.focus();
|
||||
}
|
||||
|
||||
// Optional: Show a message about validation errors
|
||||
const errorCount = invalidFields.length;
|
||||
const message = `Please fill in all required fields (${errorCount} ${errorCount === 1 ? 'error' : 'errors'} found)`;
|
||||
if (typeof Swal !== 'undefined') {
|
||||
// If SweetAlert2 is available
|
||||
Swal.fire({
|
||||
title: 'Validation Error',
|
||||
text: message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
} else {
|
||||
// Fallback to browser alert
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: Real-time validation as user switches tabs
|
||||
const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]');
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('shown.bs.tab', function() {
|
||||
const previousTabPane = document.querySelector(button.getAttribute('data-bs-target'));
|
||||
if (previousTabPane) {
|
||||
const invalidFields = previousTabPane.querySelectorAll(':invalid');
|
||||
if (invalidFields.length > 0) {
|
||||
// Add visual indicator to tab
|
||||
button.classList.add('has-error');
|
||||
} else {
|
||||
button.classList.remove('has-error');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
.json-editor-container {
|
||||
|
||||
28
eveai_app/templates/user/confirm_delete_tenant_project.html
Normal file
28
eveai_app/templates/user/confirm_delete_tenant_project.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Delete Tenant Project{% endblock %}
|
||||
|
||||
{% block content_title %}Delete Tenant Project{% endblock %}
|
||||
{% block content_description %}Are you sure you want to delete this tenant project?{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="alert alert-warning">
|
||||
<p><strong>Warning:</strong> You are about to delete the following tenant project:</p>
|
||||
<ul>
|
||||
<li><strong>Name:</strong> {{ tenant_project.name }}</li>
|
||||
<li><strong>API Key:</strong> {{ tenant_project.visual_api_key }}</li>
|
||||
<li><strong>Responsible:</strong> {{ tenant_project.responsible_email or 'Not specified' }}</li>
|
||||
</ul>
|
||||
<p>This action cannot be undone.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
{{ form.csrf_token if form }}
|
||||
<div class="form-group mt-3">
|
||||
<a href="{{ url_for('user_bp.tenant_projects') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger">Confirm Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
eveai_app/templates/user/edit_tenant_project.html
Normal file
26
eveai_app/templates/user/edit_tenant_project.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Edit Tenant Project{% endblock %}
|
||||
|
||||
{% block content_title %}Edit Tenant Project{% endblock %}
|
||||
{% block content_description %}Edit a Tenant Project. It is impossible to view of renew the existing API key.
|
||||
You need to invalidate the current project, and create a new one.
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
<!-- Render Static Fields -->
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Save Tenant Project</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
23
eveai_app/templates/user/tenant_project.html
Normal file
23
eveai_app/templates/user/tenant_project.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}Tenant Project Registration{% endblock %}
|
||||
|
||||
{% block content_title %}Register Tenant Project{% endblock %}
|
||||
{% block content_description %}Define a new tenant project to enable APIs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Register Tenant Project</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
{% endblock %}
|
||||
25
eveai_app/templates/user/tenant_projects.html
Normal file
25
eveai_app/templates/user/tenant_projects.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||
|
||||
{% block title %}Documents{% endblock %}
|
||||
|
||||
{% block content_title %}Tenant Projects{% endblock %}
|
||||
{% block content_description %}View Tenant Projects for Tenant{% endblock %}
|
||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<form method="POST" action="{{ url_for("user_bp.handle_tenant_project_selection") }}">
|
||||
{{ render_selectable_table(headers=["Tenant Project ID", "Name", "API Clue", "Responsible", "Active"], rows=rows, selectable=True, id="catalogsTable") }}
|
||||
<div class="form-group mt-3">
|
||||
<button type="submit" name="action" value="edit_tenant_project" class="btn btn-primary">Edit Tenant Project</button>
|
||||
<button type="submit" name="action" value="invalidate_tenant_project" class="btn btn-primary">Invalidate Tenant Project</button>
|
||||
<button type="submit" name="action" value="delete_tenant_project" class="btn btn-danger">Delete Tenant Project</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, "user_bp.tenant_projects") }}
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,12 @@ from flask import session
|
||||
from flask_security import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SelectField
|
||||
from wtforms.validators import DataRequired
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||
|
||||
from common.models.document import Catalog
|
||||
from common.models.user import Tenant
|
||||
from common.utils.database import Database
|
||||
|
||||
|
||||
class SessionDefaultsForm(FlaskForm):
|
||||
@@ -13,11 +18,32 @@ class SessionDefaultsForm(FlaskForm):
|
||||
tenant_name = StringField('Tenant Name', validators=[DataRequired()])
|
||||
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
||||
|
||||
# Default Catalog - initialize as a regular SelectField
|
||||
catalog = SelectField('Catalog', choices=[], validators=[Optional()])
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Set basic fields first (these don't require DB access)
|
||||
self.user_name.data = current_user.user_name
|
||||
self.user_email.data = current_user.email
|
||||
self.tenant_name.data = session.get('tenant').get('name')
|
||||
self.default_language.choices = [(lang, lang.lower()) for lang in
|
||||
session.get('tenant').get('allowed_languages')]
|
||||
self.default_language.data = session.get('default_language')
|
||||
self.default_language.data = session.get('default_language')
|
||||
|
||||
# Get a new session for catalog queries
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
tenant_session = Database(tenant_id).get_session()
|
||||
try:
|
||||
# Populate catalog choices using tenant session
|
||||
catalogs = tenant_session.query(Catalog).all()
|
||||
self.catalog.choices = [(str(c.id), c.name) for c in catalogs]
|
||||
self.catalog.choices.insert(0, ('', 'Select a Catalog')) # Add empty choice
|
||||
|
||||
# Set current catalog if exists
|
||||
catalog_id = session.get('catalog_id')
|
||||
if catalog_id:
|
||||
self.catalog.data = str(catalog_id)
|
||||
finally:
|
||||
tenant_session.close()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from flask import request, render_template, Blueprint, session, current_app, jsonify
|
||||
from flask import request, render_template, Blueprint, session, current_app, jsonify, flash, redirect
|
||||
from flask_security import roles_required, roles_accepted
|
||||
from flask_wtf.csrf import generate_csrf
|
||||
|
||||
from common.models.document import Catalog
|
||||
from common.models.user import Tenant
|
||||
from common.utils.database import Database
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from .basic_forms import SessionDefaultsForm
|
||||
|
||||
basic_bp = Blueprint('basic_bp', __name__)
|
||||
@@ -9,7 +13,7 @@ basic_bp = Blueprint('basic_bp', __name__)
|
||||
|
||||
@basic_bp.before_request
|
||||
def log_before_request():
|
||||
pass
|
||||
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
||||
|
||||
|
||||
@basic_bp.after_request
|
||||
@@ -35,12 +39,39 @@ def confirm_email_fail():
|
||||
@basic_bp.route('/session_defaults', methods=['GET', 'POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def session_defaults():
|
||||
form = SessionDefaultsForm()
|
||||
try:
|
||||
# Get tenant session
|
||||
tenant_id = session.get('tenant').get('id')
|
||||
tenant_db = Database(tenant_id)
|
||||
tenant_session = tenant_db.get_session()
|
||||
|
||||
if form.validate_on_submit():
|
||||
session['default_language'] = form.default_language.data
|
||||
try:
|
||||
form = SessionDefaultsForm()
|
||||
|
||||
return render_template('basic/session_defaults.html', form=form)
|
||||
if form.validate_on_submit():
|
||||
session['default_language'] = form.default_language.data
|
||||
if form.catalog.data:
|
||||
catalog_id = int(form.catalog.data)
|
||||
catalog = tenant_session.query(Catalog).get(catalog_id)
|
||||
if catalog:
|
||||
session['catalog_id'] = catalog.id
|
||||
session['catalog_name'] = catalog.name
|
||||
else:
|
||||
session.pop('catalog_id', None)
|
||||
session.pop('catalog_name', None)
|
||||
|
||||
flash('Session defaults updated successfully', 'success')
|
||||
return redirect(prefixed_url_for('basic_bp.index'))
|
||||
|
||||
return render_template('basic/session_defaults.html', form=form)
|
||||
|
||||
finally:
|
||||
tenant_session.close()
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error in session_defaults: {str(e)}")
|
||||
flash('Error accessing catalog data. Please ensure your session is valid.', 'danger')
|
||||
return redirect(prefixed_url_for('security_bp.login'))
|
||||
|
||||
|
||||
@basic_bp.route('/set_user_timezone', methods=['POST'])
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from flask import session, current_app, request
|
||||
from flask import session, current_app
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (StringField, BooleanField, SubmitField, DateField, IntegerField, FloatField, SelectMultipleField,
|
||||
SelectField, FieldList, FormField, TextAreaField, URLField)
|
||||
from wtforms import (StringField, BooleanField, SubmitField, DateField, IntegerField, SelectField, TextAreaField, URLField)
|
||||
from wtforms.validators import DataRequired, Length, Optional, URL, ValidationError, NumberRange
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
from flask_wtf.file import FileField, FileRequired
|
||||
import json
|
||||
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||
|
||||
from common.models.document import Catalog
|
||||
|
||||
from config.catalog_types import CATALOG_TYPES
|
||||
from config.processor_types import PROCESSOR_TYPES
|
||||
from config.retriever_types import RETRIEVER_TYPES
|
||||
from config.type_defs.catalog_types import CATALOG_TYPES
|
||||
from config.type_defs.processor_types import PROCESSOR_TYPES
|
||||
from config.type_defs.retriever_types import RETRIEVER_TYPES
|
||||
from .dynamic_form_base import DynamicFormBase
|
||||
|
||||
|
||||
@@ -179,6 +178,7 @@ class EditRetrieverForm(DynamicFormBase):
|
||||
|
||||
class AddDocumentForm(DynamicFormBase):
|
||||
file = FileField('File', validators=[FileRequired(), allowed_file])
|
||||
catalog = StringField('Catalog', render_kw={'readonly': True})
|
||||
sub_file_type = StringField('Sub File Type', validators=[Optional(), Length(max=50)])
|
||||
name = StringField('Name', validators=[Length(max=100)])
|
||||
language = SelectField('Language', choices=[], validators=[Optional()])
|
||||
@@ -193,9 +193,12 @@ class AddDocumentForm(DynamicFormBase):
|
||||
if not self.language.data:
|
||||
self.language.data = session.get('tenant').get('default_language')
|
||||
|
||||
self.catalog.data = session.get('catalog_name', '')
|
||||
|
||||
|
||||
class AddURLForm(DynamicFormBase):
|
||||
url = URLField('URL', validators=[DataRequired(), URL()])
|
||||
catalog = StringField('Catalog', render_kw={'readonly': True})
|
||||
sub_file_type = StringField('Sub File Type', validators=[Optional(), Length(max=50)])
|
||||
name = StringField('Name', validators=[Length(max=100)])
|
||||
language = SelectField('Language', choices=[], validators=[Optional()])
|
||||
@@ -210,22 +213,7 @@ class AddURLForm(DynamicFormBase):
|
||||
if not self.language.data:
|
||||
self.language.data = session.get('tenant').get('default_language')
|
||||
|
||||
|
||||
class AddURLsForm(FlaskForm):
|
||||
urls = TextAreaField('URL(s) (one per line)', validators=[DataRequired()])
|
||||
name = StringField('Name Prefix', validators=[Length(max=100)])
|
||||
language = SelectField('Language', choices=[], validators=[Optional()])
|
||||
user_context = TextAreaField('User Context', validators=[Optional()])
|
||||
valid_from = DateField('Valid from', id='form-control datepicker', validators=[Optional()])
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.language.choices = [(language, language) for language in
|
||||
session.get('tenant').get('allowed_languages')]
|
||||
if not self.language.data:
|
||||
self.language.data = session.get('tenant').get('default_language')
|
||||
self.catalog.data = session.get('catalog_name', '')
|
||||
|
||||
|
||||
class EditDocumentForm(FlaskForm):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ast
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
|
||||
from babel.messages.setuptools_frontend import update_catalog
|
||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
||||
from flask_security import roles_accepted, current_user
|
||||
from sqlalchemy import desc
|
||||
@@ -10,35 +9,33 @@ from werkzeug.utils import secure_filename
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import requests
|
||||
from requests.exceptions import SSLError
|
||||
from urllib.parse import urlparse, unquote
|
||||
import io
|
||||
import json
|
||||
|
||||
from common.models.document import Document, DocumentVersion, Catalog, Retriever, Processor
|
||||
from common.extensions import db, minio_client
|
||||
from common.extensions import db
|
||||
from common.utils.document_utils import validate_file_type, create_document_stack, start_embedding_task, process_url, \
|
||||
process_multiple_urls, get_documents_list, edit_document, \
|
||||
edit_document, \
|
||||
edit_document_version, refresh_document
|
||||
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \
|
||||
EveAIDoubleURLException
|
||||
from config.processor_types import PROCESSOR_TYPES
|
||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, AddURLsForm, \
|
||||
from config.type_defs.processor_types import PROCESSOR_TYPES
|
||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \
|
||||
CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm
|
||||
from common.utils.middleware import mw_before_request
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro, form_to_dict
|
||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||
from .document_list_view import DocumentListView
|
||||
from .document_version_list_view import DocumentVersionListView
|
||||
from config.catalog_types import CATALOG_TYPES
|
||||
from config.retriever_types import RETRIEVER_TYPES
|
||||
from config.type_defs.catalog_types import CATALOG_TYPES
|
||||
from config.type_defs.retriever_types import RETRIEVER_TYPES
|
||||
|
||||
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
|
||||
|
||||
|
||||
@document_bp.before_request
|
||||
def log_before_request():
|
||||
pass
|
||||
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
||||
|
||||
|
||||
@document_bp.after_request
|
||||
@@ -214,7 +211,7 @@ def edit_processor(processor_id):
|
||||
db.session.add(processor)
|
||||
db.session.commit()
|
||||
flash('Retriever updated successfully!', 'success')
|
||||
current_app.logger.info(f'Retriever {processor.id} updated successfully')
|
||||
current_app.logger.info(f'Processor {processor.id} updated successfully')
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update processor. Error: {str(e)}', 'danger')
|
||||
@@ -376,7 +373,7 @@ def add_document():
|
||||
form = AddDocumentForm(request.form)
|
||||
catalog_id = session.get('catalog_id', None)
|
||||
if catalog_id is None:
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs')
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
@@ -434,7 +431,7 @@ def add_url():
|
||||
form = AddURLForm(request.form)
|
||||
catalog_id = session.get('catalog_id', None)
|
||||
if catalog_id is None:
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs')
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs', 'warning')
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
@@ -547,6 +544,7 @@ def edit_document_view(document_id):
|
||||
|
||||
if form.validate_on_submit():
|
||||
updated_doc, error = edit_document(
|
||||
session.get('tenant').get('id', 0),
|
||||
document_id,
|
||||
form.name.data,
|
||||
form.valid_from.data,
|
||||
@@ -569,10 +567,8 @@ def edit_document_version_view(document_version_id):
|
||||
doc_vers = DocumentVersion.query.get_or_404(document_version_id)
|
||||
form = EditDocumentVersionForm(request.form, obj=doc_vers)
|
||||
|
||||
catalog_id = session.get('catalog_id', None)
|
||||
if catalog_id is None:
|
||||
flash('You need to set a Session Catalog before adding Documents or URLs')
|
||||
return redirect(prefixed_url_for('document_bp.catalogs'))
|
||||
doc_vers = DocumentVersion.query.get_or_404(document_version_id)
|
||||
catalog_id = doc_vers.document.catalog_id
|
||||
|
||||
catalog = Catalog.query.get_or_404(catalog_id)
|
||||
if catalog.configuration and len(catalog.configuration) > 0:
|
||||
@@ -587,6 +583,7 @@ def edit_document_version_view(document_version_id):
|
||||
catalog_properties[config] = form.get_dynamic_data(config)
|
||||
|
||||
updated_version, error = edit_document_version(
|
||||
session.get('tenant').get('id', 0),
|
||||
document_version_id,
|
||||
form.user_context.data,
|
||||
catalog_properties,
|
||||
|
||||
@@ -35,7 +35,7 @@ def license_tier():
|
||||
return render_template('entitlements/license_tier.html', form=form)
|
||||
|
||||
current_app.logger.info(f"Successfully created license tier {new_license_tier.id}")
|
||||
flash(f"Successfully created tenant license tier {new_license_tier.id}")
|
||||
flash(f"Successfully created tenant license tier {new_license_tier.id}", 'success')
|
||||
|
||||
return redirect(prefixed_url_for('entitlements_bp.view_license_tiers'))
|
||||
else:
|
||||
@@ -232,3 +232,63 @@ def view_usages():
|
||||
|
||||
# Render the users in a template
|
||||
return render_template('entitlements/view_usages.html', rows=rows, pagination=pagination)
|
||||
|
||||
|
||||
@entitlements_bp.route('/handle_usage_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def handle_usage_selection():
|
||||
usage_identification = request.form['selected_row']
|
||||
usage_id = ast.literal_eval(usage_identification).get('value')
|
||||
the_usage = LicenseUsage.query.get_or_404(usage_id)
|
||||
|
||||
action = request.form['action']
|
||||
|
||||
pass # Currently, no actions are defined
|
||||
|
||||
|
||||
@entitlements_bp.route('/view_licenses')
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def view_licenses():
|
||||
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')
|
||||
# Get current date in UTC
|
||||
current_date = dt.now(tz=tz.utc).date()
|
||||
|
||||
# Query licenses for the tenant, with ordering and active status
|
||||
query = (
|
||||
License.query.filter_by(tenant_id=tenant_id)
|
||||
.add_columns(
|
||||
License.id,
|
||||
License.start_date,
|
||||
License.end_date,
|
||||
License.license_tier,
|
||||
License.license_tier.name.label('license_tier_name'),
|
||||
((License.start_date <= current_date) &
|
||||
(or_(License.end_date.is_(None), License.end_date >= current_date))).label('active')
|
||||
)
|
||||
.order_by(License.start_date.desc())
|
||||
)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page)
|
||||
lics = pagination.items
|
||||
|
||||
# prepare table data
|
||||
rows = prepare_table_for_macro(lics, [('id', ''), ('license_tier_name', ''), ('start_date', ''), ('end_date', ''),
|
||||
('active', ''),])
|
||||
|
||||
# Render the licenses in a template
|
||||
return render_template('entitlements/view_licenses.html', rows=rows, pagination=pagination)
|
||||
|
||||
|
||||
@entitlements_bp.route('/handle_license_selection', methods=['POST'])
|
||||
@roles_accepted('Super User', 'Tenant Admin')
|
||||
def handle_license_selection():
|
||||
license_identification = request.form['selected_row']
|
||||
license_id = ast.literal_eval(license_identification).get('value')
|
||||
the_license = LicenseUsage.query.get_or_404(license_id)
|
||||
|
||||
action = request.form['action']
|
||||
|
||||
pass # Currently, no actions are defined
|
||||
@@ -1,17 +1,12 @@
|
||||
from flask import session, current_app, request
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (StringField, BooleanField, SubmitField, DateField, IntegerField, FloatField, SelectMultipleField,
|
||||
SelectField, FieldList, FormField, TextAreaField, URLField)
|
||||
from wtforms.validators import DataRequired, Length, Optional, URL, ValidationError, NumberRange
|
||||
from flask_wtf.file import FileField, FileAllowed, FileRequired
|
||||
import json
|
||||
from wtforms import (StringField, BooleanField, SelectField, TextAreaField)
|
||||
from wtforms.validators import DataRequired, Length
|
||||
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
|
||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
||||
|
||||
from common.models.document import Retriever
|
||||
|
||||
from config.catalog_types import CATALOG_TYPES
|
||||
from config.specialist_types import SPECIALIST_TYPES
|
||||
from config.type_defs.specialist_types import SPECIALIST_TYPES
|
||||
from .dynamic_form_base import DynamicFormBase
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import ast
|
||||
import os
|
||||
from datetime import datetime as dt, timezone as tz
|
||||
|
||||
import chardet
|
||||
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
|
||||
from flask_security import roles_accepted, current_user
|
||||
from flask_security import roles_accepted
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import joinedload
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import requests
|
||||
from requests.exceptions import SSLError
|
||||
from urllib.parse import urlparse
|
||||
import io
|
||||
|
||||
from common.models.document import Embedding, DocumentVersion, Retriever
|
||||
from common.models.interaction import ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever
|
||||
from common.extensions import db
|
||||
from common.utils.document_utils import set_logging_information, update_logging_information
|
||||
from config.specialist_types import SPECIALIST_TYPES
|
||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm
|
||||
from config.type_defs.specialist_types import SPECIALIST_TYPES
|
||||
from common.utils.middleware import mw_before_request
|
||||
from common.utils.celery_utils import current_celery
|
||||
from common.utils.nginx_utils import prefixed_url_for
|
||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||
from .interaction_forms import SpecialistForm, EditSpecialistForm
|
||||
@@ -32,7 +21,7 @@ interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction'
|
||||
|
||||
@interaction_bp.before_request
|
||||
def log_before_request():
|
||||
pass
|
||||
current_app.logger.debug(f'Before request: {request.path} =====================================')
|
||||
|
||||
|
||||
@interaction_bp.after_request
|
||||
|
||||
@@ -11,11 +11,12 @@ from itsdangerous import URLSafeTimedSerializer
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from common.models.user import User
|
||||
from common.utils.eveai_exceptions import EveAIException
|
||||
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
|
||||
from common.utils.security import set_tenant_session_data, is_valid_tenant
|
||||
|
||||
security_bp = Blueprint('security_bp', __name__)
|
||||
|
||||
@@ -40,11 +41,15 @@ def login():
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
if form.validate_on_submit():
|
||||
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', 'danger')
|
||||
current_app.logger.error(f'Failed to login user')
|
||||
return redirect(prefixed_url_for('security_bp.login'))
|
||||
try:
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
if user is None or not verify_and_update_password(form.password.data, user):
|
||||
raise EveAIException('Invalid email or password')
|
||||
is_valid_tenant(user.tenant_id)
|
||||
except EveAIException as e:
|
||||
flash(f'Failed to login user: {str(e)}', 'danger')
|
||||
current_app.logger.error(f'Failed to login user: {str(e)}')
|
||||
abort(401)
|
||||
|
||||
if login_user(user):
|
||||
current_app.logger.info(f'Login successful! Current User is {current_user.email}')
|
||||
@@ -55,7 +60,7 @@ def login():
|
||||
return redirect(prefixed_url_for('user_bp.tenant_overview'))
|
||||
else:
|
||||
flash('Invalid username or password', 'danger')
|
||||
current_app.logger.error(f'Failed to login user {user.email}')
|
||||
current_app.logger.error(f'Invalid username or password for given email: {user.email}')
|
||||
abort(401)
|
||||
else:
|
||||
current_app.logger.error(f'Invalid login form: {form.errors}')
|
||||
|
||||
@@ -6,51 +6,24 @@ from wtforms.validators import DataRequired, Length, Email, NumberRange, Optiona
|
||||
import pytz
|
||||
|
||||
from common.models.user import Role
|
||||
from config.type_defs.service_types import SERVICE_TYPES
|
||||
|
||||
|
||||
class TenantForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=80)])
|
||||
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
|
||||
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||
# language fields
|
||||
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
|
||||
allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[DataRequired()])
|
||||
# invoicing fields
|
||||
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
|
||||
usage_email = EmailField('Usage Email', validators=[DataRequired(), Email()])
|
||||
# Timezone
|
||||
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
|
||||
# RAG context
|
||||
rag_context = TextAreaField('RAG Context', validators=[Optional()])
|
||||
# Tenant Type
|
||||
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
|
||||
# LLM fields
|
||||
embedding_model = SelectField('Embedding Model', choices=[], validators=[DataRequired()])
|
||||
llm_model = SelectField('Large Language Model', choices=[], validators=[DataRequired()])
|
||||
# Embedding variables
|
||||
html_tags = StringField('HTML Tags', validators=[DataRequired()],
|
||||
default='p, h1, h2, h3, h4, h5, h6, li')
|
||||
html_end_tags = StringField('HTML End Tags', validators=[DataRequired()],
|
||||
default='p, li')
|
||||
html_included_elements = StringField('HTML Included Elements', validators=[Optional()])
|
||||
html_excluded_elements = StringField('HTML Excluded Elements', validators=[Optional()])
|
||||
html_excluded_classes = StringField('HTML Excluded Classes', validators=[Optional()])
|
||||
min_chunk_size = IntegerField('Minimum Chunk Size (2000)', validators=[NumberRange(min=0), Optional()], default=2000)
|
||||
max_chunk_size = IntegerField('Maximum Chunk Size (3000)', validators=[NumberRange(min=0), Optional()], default=3000)
|
||||
# Embedding Search variables
|
||||
es_k = IntegerField('Limit for Searching Embeddings (5)',
|
||||
default=5,
|
||||
validators=[NumberRange(min=0)])
|
||||
es_similarity_threshold = FloatField('Similarity Threshold for Searching Embeddings (0.5)',
|
||||
default=0.5,
|
||||
validators=[NumberRange(min=0, max=1)])
|
||||
# Chat Variables
|
||||
chat_RAG_temperature = FloatField('RAG Temperature', default=0.3, validators=[NumberRange(min=0, max=1)])
|
||||
chat_no_RAG_temperature = FloatField('No RAG Temperature', default=0.5, validators=[NumberRange(min=0, max=1)])
|
||||
fallback_algorithms = SelectMultipleField('Fallback Algorithms', choices=[], validators=[Optional()])
|
||||
# Tuning variables
|
||||
embed_tuning = BooleanField('Enable Embedding Tuning', default=False)
|
||||
rag_tuning = BooleanField('Enable RAG Tuning', default=False)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -66,8 +39,6 @@ class TenantForm(FlaskForm):
|
||||
self.embedding_model.choices = [(model, model) for model in current_app.config['SUPPORTED_EMBEDDINGS']]
|
||||
self.llm_model.choices = [(model, model) for model in current_app.config['SUPPORTED_LLMS']]
|
||||
# Initialize fallback algorithms
|
||||
self.fallback_algorithms.choices = \
|
||||
[(algorithm, algorithm.lower()) for algorithm in current_app.config['FALLBACK_ALGORITHMS']]
|
||||
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||
|
||||
|
||||
@@ -79,6 +50,8 @@ class BaseUserForm(FlaskForm):
|
||||
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)
|
||||
is_primary_contact = BooleanField('Primary Contact')
|
||||
is_financial_contact = BooleanField('Financial Contact')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BaseUserForm, self).__init__(*args, **kwargs)
|
||||
@@ -90,6 +63,12 @@ class CreateUserForm(BaseUserForm):
|
||||
|
||||
|
||||
class EditUserForm(BaseUserForm):
|
||||
# Some R/O informational fields
|
||||
confirmed_at = DateField('Confirmed At', id='form-control datepicker', validators=[Optional()],
|
||||
render_kw={'readonly': True})
|
||||
last_login_at = DateField('Last Login At', id='form-control datepicker', validators=[Optional()],
|
||||
render_kw={'readonly': True})
|
||||
login_count = IntegerField('Login Count', validators=[Optional()], render_kw={'readonly': True})
|
||||
submit = SubmitField('Save User')
|
||||
|
||||
|
||||
@@ -121,4 +100,30 @@ class TenantSelectionForm(FlaskForm):
|
||||
self.types.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
|
||||
|
||||
|
||||
class TenantProjectForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()])
|
||||
unencrypted_api_key = StringField('Unencrypted API Key', validators=[DataRequired()], render_kw={'readonly': True})
|
||||
visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True})
|
||||
active = BooleanField('Active', validators=[DataRequired()], default=True)
|
||||
responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Initialize choices for the services field
|
||||
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
||||
|
||||
|
||||
class EditTenantProjectForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
services = SelectMultipleField('Allowed Services', choices=[], validators=[DataRequired()])
|
||||
visual_api_key = StringField('Visual API Key', validators=[DataRequired()], render_kw={'readonly': True})
|
||||
active = BooleanField('Active', validators=[DataRequired()], default=True)
|
||||
responsible_email = EmailField('Responsible Email', validators=[Optional(), Email()])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Initialize choices for the services field
|
||||
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
||||
|
||||
@@ -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