- 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:
Josako
2024-11-21 17:24:33 +01:00
parent 4c009949b3
commit 7702a6dfcc
72 changed files with 2338 additions and 503 deletions

View File

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

View 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 %}

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

View 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>-->
<!-- &lt;!&ndash; Additional buttons can be added here for other actions &ndash;&gt;-->
<!-- </div>-->
</form>
{% endblock %}
{% block content_footer %}
{{ render_pagination(pagination, 'entitlements_bp.view_licenses') }}
{% endblock %}
{% block scripts %}
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View 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 %}

View 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 %}

View 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 %}

View 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 %}