diff --git a/config/config.py b/config/config.py index 8f59a3a..dc138cc 100644 --- a/config/config.py +++ b/config/config.py @@ -135,15 +135,6 @@ class Config(object): "LLM": {"name": "LLM", "description": "Algorithm using information integrated in the used LLM"} } - # flask-mailman settings - MAIL_SERVER = environ.get('MAIL_SERVER') - MAIL_PORT = int(environ.get('MAIL_PORT', 465)) - MAIL_USE_TLS = False - MAIL_USE_SSL = True - MAIL_USERNAME = environ.get('MAIL_USERNAME') - MAIL_PASSWORD = environ.get('MAIL_PASSWORD') - MAIL_DEFAULT_SENDER = ('Evie', MAIL_USERNAME) - # Email settings for API key notifications PROMOTIONAL_IMAGE_URL = 'https://askeveai.com/wp-content/uploads/2024/07/Evie-Call-scaled.jpg' # Replace with your actual URL @@ -175,6 +166,13 @@ class Config(object): PUSH_GATEWAY_PORT = environ.get('PUSH_GATEWAY_PORT', '9091') PUSH_GATEWAY_URL = f"{PUSH_GATEWAY_HOST}:{PUSH_GATEWAY_PORT}" + # Scaleway parameters + SW_EMAIL_ACCESS_KEY = environ.get('SW_EMAIL_ACCESS_KEY') + SW_EMAIL_SECRET_KEY = environ.get('SW_EMAIL_SECRET_KEY') + SW_EMAIL_SENDER = environ.get('SW_EMAIL_SENDER') + SW_EMAIL_NAME = environ.get('SW_EMAIL_NAME') + SW_PROJECT = environ.get('SW_PROJECT') + class DevConfig(Config): DEVELOPMENT = True @@ -252,15 +250,6 @@ class ProdConfig(Config): WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS - # flask-mailman settings - MAIL_SERVER = 'mail.askeveai.com' - MAIL_PORT = 587 - MAIL_USE_TLS = True - MAIL_USE_SSL = False - MAIL_DEFAULT_SENDER = ('Evie Admin', 'evie_admin@askeveai.com') - MAIL_USERNAME = environ.get('MAIL_USERNAME') - MAIL_PASSWORD = environ.get('MAIL_PASSWORD') - # Define the nginx prefix used for the specific apps EVEAI_APP_LOCATION_PREFIX = '/admin' EVEAI_CHAT_LOCATION_PREFIX = '/chat' diff --git a/config/logging_config.py b/config/logging_config.py index a8605f1..fdecf3d 100644 --- a/config/logging_config.py +++ b/config/logging_config.py @@ -270,14 +270,6 @@ LOGGING = { 'backupCount': 2, 'formatter': 'standard', }, - 'file_mailman': { - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': 'logs/mailman.log', - 'maxBytes': 1024 * 1024 * 1, # 1MB - 'backupCount': 2, - 'formatter': 'standard', - }, 'file_security': { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', @@ -389,11 +381,6 @@ LOGGING = { 'level': 'DEBUG', 'propagate': False }, - 'mailman': { # logger for the mailman - 'handlers': ['file_mailman', 'graylog', ] if env == 'production' else ['file_mailman', ], - 'level': 'DEBUG', - 'propagate': False - }, 'security': { # logger for the security 'handlers': ['file_security', 'graylog', ] if env == 'production' else ['file_security', ], 'level': 'DEBUG', diff --git a/docker/compose_dev.yaml b/docker/compose_dev.yaml index 39356a1..2bc6566 100644 --- a/docker/compose_dev.yaml +++ b/docker/compose_dev.yaml @@ -18,10 +18,6 @@ x-common-variables: &common-variables FLASK_DEBUG: true SECRET_KEY: '97867c1491bea5ee6a8e8436eb11bf2ba6a69ff53ab1b17ecba450d0f2e572e1' SECURITY_PASSWORD_SALT: '228614859439123264035565568761433607235' - MAIL_USERNAME: evie@askeveai.com - MAIL_PASSWORD: 'D**0z@UGfJOI@yv3eC5' - MAIL_SERVER: mail.flow-it.net - MAIL_PORT: 465 REDIS_URL: redis REDIS_PORT: '6379' FLOWER_USER: 'Felucia' @@ -41,6 +37,11 @@ x-common-variables: &common-variables CREWAI_STORAGE_DIR: "/app/crewai_storage" PUSH_GATEWAY_HOST: "pushgateway" PUSH_GATEWAY_PORT: "9091" + SW_EMAIL_ACCESS_KEY: "SCWFMQ871RE4XGF04SW0" + SW_EMAIL_SECRET_KEY: "ec84604c-e2d4-4b0d-a120-40420693f42a" + SW_EMAIL_SENDER: "admin_dev@mail.askeveai.be" + SW_EMAIL_NAME: "Evie Admin (dev)" + SW_PROJECT: "f282f55a-ea52-4538-a979-5bcb890717ab" services: nginx: diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index 1770c97..a2664cc 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -6,7 +6,7 @@ from flask_security.signals import user_authenticated from werkzeug.middleware.proxy_fix import ProxyFix import logging.config -from common.extensions import (db, migrate, bootstrap, security, mail, login_manager, cors, csrf, session, +from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session, minio_client, simple_encryption, metrics, cache_manager) from common.models.user import User, Role, Tenant, TenantDomain import common.models.interaction @@ -19,7 +19,7 @@ from .errors import register_error_handlers from common.utils.celery_utils import make_celery, init_celery from common.utils.template_filters import register_filters from config.config import get_config -from eveai_app.views.security_forms import ResetPasswordForm +from eveai_app.views.security_forms import ResetPasswordForm, ForgotPasswordForm def create_app(config_file=None): @@ -40,6 +40,7 @@ def create_app(config_file=None): app.config['SESSION_KEY_PREFIX'] = 'eveai_app_' app.config['SECURITY_RESET_PASSWORD_FORM'] = ResetPasswordForm + app.config['SECURITY_FORGOT_PASSWORD_FORM'] = ForgotPasswordForm try: os.makedirs(app.instance_path) @@ -79,8 +80,6 @@ def create_app(config_file=None): # Debugging settings if app.config['DEBUG'] is True: app.logger.setLevel(logging.DEBUG) - mail_logger = logging.getLogger('flask_mailman') - mail_logger.setLevel(logging.DEBUG) security_logger = logging.getLogger('flask_security') security_logger.setLevel(logging.DEBUG) sqlalchemy_logger = logging.getLogger('sqlalchemy.engine') @@ -117,7 +116,6 @@ def register_extensions(app): db.init_app(app) migrate.init_app(app, db) bootstrap.init_app(app) - mail.init_app(app) csrf.init_app(app) login_manager.init_app(app) cors.init_app(app) diff --git a/eveai_app/templates/entitlements/license.html b/eveai_app/templates/entitlements/license.html index 829f6a6..581a127 100644 --- a/eveai_app/templates/entitlements/license.html +++ b/eveai_app/templates/entitlements/license.html @@ -38,21 +38,21 @@
- {% set storage_fields = ['max_storage_mb', 'additional_storage_price', 'additional_storage_bucket'] %} + {% set storage_fields = ['max_storage_mb', 'additional_storage_allowed', 'additional_storage_price', 'additional_storage_bucket'] %} {% for field in form %} {{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=storage_fields) }} {% endfor %}
- {% set embedding_fields = ['included_embedding_mb', 'additional_embedding_price', 'additional_embedding_bucket', 'overage_embedding'] %} + {% set embedding_fields = ['included_embedding_mb', 'additional_embedding_allowed', 'additional_embedding_price', 'additional_embedding_bucket', 'overage_embedding'] %} {% for field in form %} {{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=embedding_fields) }} {% endfor %}
- {% set interaction_fields = ['included_interaction_tokens', 'additional_interaction_token_price', 'additional_interaction_bucket', 'overage_interaction'] %} + {% set interaction_fields = ['included_interaction_tokens', 'additional_interaction_allowed', 'additional_interaction_token_price', 'additional_interaction_bucket', 'overage_interaction'] %} {% for field in form %} {{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=interaction_fields) }} {% endfor %} diff --git a/eveai_app/templates/security/forgot_password.html b/eveai_app/templates/security/forgot_password.html index 0fd4243..2f3295e 100644 --- a/eveai_app/templates/security/forgot_password.html +++ b/eveai_app/templates/security/forgot_password.html @@ -7,7 +7,7 @@ {% block content %} {% include "security/_messages.html" %} -
+ {{ forgot_password_form.hidden_tag() }}

diff --git a/eveai_app/templates/security/login_user.html b/eveai_app/templates/security/login_user.html index 6e08bf3..b3e70e4 100644 --- a/eveai_app/templates/security/login_user.html +++ b/eveai_app/templates/security/login_user.html @@ -26,8 +26,7 @@ {#

#} {% endblock %} {% block content_footer %} - First time here? Forgot your password? - Request new password + Forgot your password? Contact your administrator to send you a password reset link. {% endblock %} {#{{ url_for_security('reset_password', token=reset_password_token) }}#} \ No newline at end of file diff --git a/eveai_app/views/entitlements_forms.py b/eveai_app/views/entitlements_forms.py index fefd359..de767b2 100644 --- a/eveai_app/views/entitlements_forms.py +++ b/eveai_app/views/entitlements_forms.py @@ -51,10 +51,12 @@ class LicenseForm(FlaskForm): yearly_payment = BooleanField('Yearly Payment', default=False) basic_fee = FloatField('Basic Fee', validators=[InputRequired(), NumberRange(min=0)]) max_storage_mb = IntegerField('Max Storage (MiB)', validators=[DataRequired(), NumberRange(min=1)]) + additional_storage_allowed = BooleanField('Additional Storage Allowed', default=True) additional_storage_price = FloatField('Additional Storage Token Fee', validators=[InputRequired(), NumberRange(min=0)]) additional_storage_bucket = IntegerField('Additional Storage Bucket Size (MiB)', validators=[DataRequired(), NumberRange(min=1)]) + additional_embedding_allowed = BooleanField('Additional Embedding Allowed', default=True) included_embedding_mb = IntegerField('Included Embedding Tokens (MiB)', validators=[DataRequired(), NumberRange(min=1)]) additional_embedding_price = FloatField('Additional Embedding Token Fee', @@ -63,6 +65,7 @@ class LicenseForm(FlaskForm): validators=[DataRequired(), NumberRange(min=1)]) included_interaction_tokens = IntegerField('Included Interaction Tokens (M Tokens)', validators=[DataRequired(), NumberRange(min=1)]) + additional_interaction_allowed = BooleanField('Additional Interaction Allowed', default=True) additional_interaction_token_price = FloatField('Additional Interaction Token Fee', validators=[InputRequired(), NumberRange(min=0)]) additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size (M Tokens)', diff --git a/eveai_app/views/entitlements_views.py b/eveai_app/views/entitlements_views.py index 2ca0034..c88e602 100644 --- a/eveai_app/views/entitlements_views.py +++ b/eveai_app/views/entitlements_views.py @@ -152,8 +152,9 @@ def create_license(license_tier_id): tenant_id = session.get('tenant').get('id') currency = session.get('tenant').get('currency') - if current_user_has_role("Partner Admin"): # The Partner Admin can only set the end date - readonly_fields = [field.name for field in form if (field.name != 'end_date' and field.name != 'start_date')] + if current_user_has_role("Partner Admin"): # The Partner Admin can only set start & end dates, and allowed fields + readonly_fields = [field.name for field in form if (field.name != 'end_date' and field.name != 'start_date' and + not field.name.endswith('allowed'))] if request.method == 'GET': # Fetch the LicenseTier diff --git a/eveai_app/views/security_forms.py b/eveai_app/views/security_forms.py index 527282e..8126e11 100644 --- a/eveai_app/views/security_forms.py +++ b/eveai_app/views/security_forms.py @@ -2,10 +2,6 @@ from flask import current_app from flask_wtf import FlaskForm from wtforms import PasswordField, SubmitField, StringField from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, EqualTo -from flask_security.forms import ForgotPasswordForm -from flask_security.utils import send_mail, config_value - -from common.utils.nginx_utils import prefixed_url_for class SetPasswordForm(FlaskForm): @@ -14,7 +10,7 @@ class SetPasswordForm(FlaskForm): submit = SubmitField('Set Password') -class RequestResetForm(FlaskForm): +class ForgotPasswordForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Email()]) submit = SubmitField('Request Password Reset') diff --git a/eveai_app/views/security_views.py b/eveai_app/views/security_views.py index a8088bd..e35082e 100644 --- a/eveai_app/views/security_views.py +++ b/eveai_app/views/security_views.py @@ -13,7 +13,7 @@ from sqlalchemy.exc import SQLAlchemyError from common.models.user import User from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense from common.utils.nginx_utils import prefixed_url_for -from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, RequestResetForm +from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, ForgotPasswordForm 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, is_valid_tenant @@ -111,16 +111,16 @@ def confirm_email(token): return redirect(prefixed_url_for('basic_bp.confirm_email_ok')) -@security_bp.route('/reset_password_request', methods=['GET', 'POST']) -def reset_password_request(): - form = RequestResetForm() +@security_bp.route('/forgot_password', methods=['GET', 'POST']) +def forgot_password(): + form = ForgotPasswordForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user: send_reset_email(user) flash('An email with instructions to reset your password has been sent.', 'info') return redirect(prefixed_url_for('security_bp.login')) - return render_template('security/reset_password_request.html', form=form) + return render_template('security/forgot_password.html', form=form) @security_bp.route('/reset_password/', methods=['GET', 'POST']) diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index 244232d..d7f02b0 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -1,7 +1,6 @@ import uuid from datetime import datetime as dt, timezone as tz from flask import request, redirect, flash, render_template, Blueprint, session, current_app -from flask_mailman import EmailMessage from flask_security import roles_accepted, current_user from sqlalchemy.exc import SQLAlchemyError, IntegrityError import ast @@ -21,6 +20,7 @@ from common.utils.eveai_exceptions import EveAIException from common.utils.document_utils import set_logging_information, update_logging_information from common.services.tenant_services import TenantServices from common.services.user_services import UserServices +from common.utils.mail_utils import send_email user_bp = Blueprint('user_bp', __name__, url_prefix='/user') @@ -670,20 +670,15 @@ def send_api_key_notification(tenant_id, tenant_name, project_name, api_key, ser } try: + # Create email message - msg = EmailMessage( + msg = send_email( 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] + html=render_template('email/api_key_notification.html', **context), + to_email=recipient_email, + to_name=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 diff --git a/migrations/public/versions/fa6113ce4306_add_allowed_fields_for_overrun_in_.py b/migrations/public/versions/fa6113ce4306_add_allowed_fields_for_overrun_in_.py new file mode 100644 index 0000000..2c0873b --- /dev/null +++ b/migrations/public/versions/fa6113ce4306_add_allowed_fields_for_overrun_in_.py @@ -0,0 +1,48 @@ +"""Add Allowed fields for overrun in License + +Revision ID: fa6113ce4306 +Revises: 9ed466e9756b +Create Date: 2025-05-08 16:05:55.612416 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'fa6113ce4306' +down_revision = '9ed466e9756b' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('license', schema=None) as batch_op: + batch_op.add_column( + sa.Column('additional_storage_allowed', sa.Boolean(), nullable=True)) + batch_op.add_column( + sa.Column('additional_embedding_allowed', sa.Boolean(), nullable=True)) + batch_op.add_column( + sa.Column('additional_interaction_allowed', sa.Boolean(), nullable=True)) + + op.execute(""" + UPDATE license + SET additional_storage_allowed = TRUE, + additional_embedding_allowed = TRUE, + additional_interaction_allowed = TRUE + """) + + with op.batch_alter_table('license', schema=None) as batch_op: + batch_op.alter_column('additional_storage_allowed', nullable=False) + batch_op.alter_column('additional_embedding_allowed', nullable=False) + batch_op.alter_column('additional_interaction_allowed', nullable=False) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('license', schema=None) as batch_op: + batch_op.drop_column('additional_interaction_allowed') + batch_op.drop_column('additional_embedding_allowed') + batch_op.drop_column('additional_storage_allowed') + + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 7fca92b..0452266 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ Flask-Bootstrap~=3.3.7.1 Flask-Cors~=5.0.1 Flask-JWT-Extended~=4.7.1 Flask-Login~=0.6.3 -flask-mailman~=1.1.1 Flask-Migrate~=4.1.0 Flask-Principal~=0.4.0 Flask-Security-Too~=5.6.2 @@ -89,4 +88,7 @@ termcolor~=2.5.0 mistral-common~=1.5.3 mistralai~=1.6.0 contextvars~=2.4 -pandas~=2.2.3 \ No newline at end of file +pandas~=2.2.3 +prometheus_client~=0.21.1 +scaleway~=2.9.0 +html2text~=2025.4.15 \ No newline at end of file