From 9757830bc402a0b6c907b2159c764c05ce6ad84a Mon Sep 17 00:00:00 2001 From: Josako Date: Wed, 21 Aug 2024 14:59:56 +0200 Subject: [PATCH] Correct functions for creating new users, confirming email, resetting password and forgot password. --- common/extensions.py | 3 +- common/models/user.py | 2 +- common/utils/security_utils.py | 56 +++++++++++++- config/config.py | 24 +++--- docker/compose_dev.yaml | 4 +- docker/compose_stackhero.yaml | 4 +- eveai_app/__init__.py | 1 + .../templates/email/forgot_password.html | 13 ++++ .../security/email/reset_instructions.html | 13 ++++ .../templates/security/forgot_password.html | 12 ++- eveai_app/templates/security/login_user.html | 5 +- .../templates/security/reset_password.html | 6 +- eveai_app/templates/user/view_users.html | 1 + eveai_app/views/security_forms.py | 6 +- eveai_app/views/security_views.py | 2 + eveai_app/views/user_views.py | 33 ++++----- logs/eveai_app.log | 3 + ...ed_fs_uniquifier_required_password_not_.py | 74 +++++++++++++++++++ ..._uniquifier_not_required_when_creating_.py | 68 +++++++++++++++++ nginx/nginx.conf | 4 + 20 files changed, 291 insertions(+), 43 deletions(-) create mode 100644 eveai_app/templates/email/forgot_password.html create mode 100644 eveai_app/templates/security/email/reset_instructions.html create mode 100644 migrations/public/versions/229774547fed_fs_uniquifier_required_password_not_.py create mode 100644 migrations/public/versions/a39d2e378ccf_uniquifier_not_required_when_creating_.py diff --git a/common/extensions.py b/common/extensions.py index f36134b..deeafdd 100644 --- a/common/extensions.py +++ b/common/extensions.py @@ -10,10 +10,11 @@ from flask_jwt_extended import JWTManager from flask_session import Session from flask_wtf import CSRFProtect -# from .utils.key_encryption import JosKMSClient +from .utils.nginx_utils import prefixed_url_for from .utils.simple_encryption import SimpleEncryption from .utils.minio_utils import MinioClient + # Create extensions db = SQLAlchemy() migrate = Migrate() diff --git a/common/models/user.py b/common/models/user.py index 69e17f2..c700613 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -126,7 +126,7 @@ class User(db.Model, UserMixin): id = db.Column(db.Integer, primary_key=True) user_name = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(255), unique=True, nullable=False) - password = db.Column(db.String(255), nullable=False) + password = db.Column(db.String(255), nullable=True) first_name = db.Column(db.String(80), nullable=False) last_name = db.Column(db.String(80), nullable=False) active = db.Column(db.Boolean) diff --git a/common/utils/security_utils.py b/common/utils/security_utils.py index 4a3a897..f86bad1 100644 --- a/common/utils/security_utils.py +++ b/common/utils/security_utils.py @@ -1,6 +1,7 @@ from flask import current_app, render_template from flask_mailman import EmailMessage from itsdangerous import URLSafeTimedSerializer +import socket from common.utils.nginx_utils import prefixed_url_for @@ -35,15 +36,66 @@ def generate_confirmation_token(email): def send_confirmation_email(user): current_app.logger.debug(f'Sending confirmation email to {user.email}') + + if not test_smtp_connection(): + raise Exception("Failed to connect to SMTP server") + token = generate_confirmation_token(user.email) confirm_url = prefixed_url_for('security_bp.confirm_email', token=token, _external=True) current_app.logger.debug(f'Confirmation URL: {confirm_url}') + html = render_template('email/activate.html', confirm_url=confirm_url) - send_email(user.email, "Confirm your email", html) + subject = "Please confirm your email" + + try: + send_email(user.email, "Confirm your email", html) + current_app.logger.info(f'Confirmation email sent to {user.email}') + except Exception as e: + current_app.logger.error(f'Failed to send confirmation email to {user.email}. Error: {str(e)}') + raise def send_reset_email(user): + current_app.logger.debug(f'Sending reset email to {user.email}') token = generate_reset_token(user.email) reset_url = prefixed_url_for('security_bp.reset_password', token=token, _external=True) + current_app.logger.debug(f'Reset URL: {reset_url}') + html = render_template('email/reset_password.html', reset_url=reset_url) - send_email(user.email, "Reset Your Password", html) + subject = "Reset Your Password" + + try: + send_email(user.email, "Reset Your Password", html) + current_app.logger.info(f'Reset email sent to {user.email}') + except Exception as e: + current_app.logger.error(f'Failed to send reset email to {user.email}. Error: {str(e)}') + raise + + +def test_smtp_connection(): + try: + current_app.logger.info(f"Attempting to resolve google.com...") + google_ip = socket.gethostbyname('google.com') + current_app.logger.info(f"Successfully resolved google.com to {google_ip}") + except Exception as e: + current_app.logger.error(f"Failed to resolve google.com: {str(e)}") + + try: + smtp_server = current_app.config['MAIL_SERVER'] + current_app.logger.info(f"Attempting to resolve {smtp_server}...") + smtp_ip = socket.gethostbyname(smtp_server) + current_app.logger.info(f"Successfully resolved {smtp_server} to {smtp_ip}") + except Exception as e: + current_app.logger.error(f"Failed to resolve {smtp_server}: {str(e)}") + + try: + smtp_server = current_app.config['MAIL_SERVER'] + smtp_port = current_app.config['MAIL_PORT'] + sock = socket.create_connection((smtp_server, smtp_port), timeout=10) + sock.close() + current_app.logger.info(f"Successfully connected to SMTP server {smtp_server}:{smtp_port}") + return True + except Exception as e: + current_app.logger.error(f"Failed to connect to SMTP server: {str(e)}") + return False + diff --git a/config/config.py b/config/config.py index d37db88..50a90a3 100644 --- a/config/config.py +++ b/config/config.py @@ -3,6 +3,7 @@ from datetime import timedelta import redis from common.utils.prompt_loader import load_prompt_templates +from eveai_app.views.security_forms import ResetPasswordForm basedir = path.abspath(path.dirname(__file__)) @@ -41,6 +42,11 @@ class Config(object): SECURITY_POST_LOGIN_VIEW = '/user/tenant_overview' SECURITY_RECOVERABLE = True SECURITY_EMAIL_SENDER = "eveai_super@flow-it.net" + SECURITY_EMAIL_SUBJECT_PASSWORD_RESET = 'Reset Your Password' + SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE = 'Your Password Has Been Reset' + SECURITY_EMAIL_PLAINTEXT = False + SECURITY_EMAIL_HTML = True + SECURITY_RESET_PASSWORD_FORM = ResetPasswordForm # Ensure Flask-Security-Too is handling CSRF tokens when behind a proxy SECURITY_CSRF_PROTECT_MECHANISMS = ['session'] @@ -123,6 +129,15 @@ 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 = ('eveAI Admin', MAIL_USERNAME) + class DevConfig(Config): DEVELOPMENT = True @@ -138,15 +153,6 @@ class DevConfig(Config): SQLALCHEMY_DATABASE_URI = f'postgresql+pg8000://{DB_USER}:{DB_PASS}@{DB_HOST}:5432/{DB_NAME}' SQLALCHEMY_BINDS = {'public': SQLALCHEMY_DATABASE_URI} - # flask-mailman settings - MAIL_SERVER = 'mail.flow-it.net' - MAIL_PORT = 587 - MAIL_USE_TLS = True - MAIL_USE_SSL = False - MAIL_DEFAULT_SENDER = ('eveAI Admin', 'eveai_admin@flow-it.net') - 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/docker/compose_dev.yaml b/docker/compose_dev.yaml index 3e2b28f..e2ef793 100644 --- a/docker/compose_dev.yaml +++ b/docker/compose_dev.yaml @@ -19,7 +19,9 @@ x-common-variables: &common-variables SECRET_KEY: '97867c1491bea5ee6a8e8436eb11bf2ba6a69ff53ab1b17ecba450d0f2e572e1' SECURITY_PASSWORD_SALT: '228614859439123264035565568761433607235' MAIL_USERNAME: eveai_super@flow-it.net - MAIL_PASSWORD: '$6xsWGbNtx$CFMQZqc*' + MAIL_PASSWORD: '$$6xsWGbNtx$$CFMQZqc*' + MAIL_SERVER: mail.flow-it.net + MAIL_PORT: 465 OPENAI_API_KEY: 'sk-proj-8R0jWzwjL7PeoPyMhJTZT3BlbkFJLb6HfRB2Hr9cEVFWEhU7' GROQ_API_KEY: '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' ANTHROPIC_API_KEY: 'sk-ant-api03-c2TmkzbReeGhXBO5JxNH6BJNylRDonc9GmZd0eRbrvyekec2' diff --git a/docker/compose_stackhero.yaml b/docker/compose_stackhero.yaml index 0f4ac01..009407b 100644 --- a/docker/compose_stackhero.yaml +++ b/docker/compose_stackhero.yaml @@ -11,7 +11,7 @@ x-common-variables: &common-variables DB_HOST: bswnz4.stackhero-network.com DB_USER: luke_skywalker - DB_PASS: 2MK&1rHmWEydE2rFuJLq*ls%tdkPAk2 + DB_PASS: '2MK&1rHmWEydE2rFuJLq*ls%tdkPAk2' DB_NAME: eveai DB_PORT: '5945' FLASK_ENV: production @@ -20,6 +20,8 @@ x-common-variables: &common-variables SECURITY_PASSWORD_SALT: '166448071751628781809462050022558634074' MAIL_USERNAME: 'evie_admin@askeveai.com' MAIL_PASSWORD: 's5D%R#y^v!s&6Z^i0k&' + MAIL_SERVER: mail.askeveai.com + MAIL_PORT: 465 REDIS_USER: eveai REDIS_PASS: 'jHliZwGD36sONgbm0fc6SOpzLbknqq4RNF8K' REDIS_URL: 8bciqc.stackhero-network.com diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index 630f8d9..e43ba8f 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -10,6 +10,7 @@ from common.extensions import (db, migrate, bootstrap, security, mail, login_man minio_client, simple_encryption) from common.models.user import User, Role, Tenant, TenantDomain import common.models.interaction +from common.utils.nginx_utils import prefixed_url_for from config.logging_config import LOGGING from common.utils.security import set_tenant_session_data from .errors import register_error_handlers diff --git a/eveai_app/templates/email/forgot_password.html b/eveai_app/templates/email/forgot_password.html new file mode 100644 index 0000000..7838c41 --- /dev/null +++ b/eveai_app/templates/email/forgot_password.html @@ -0,0 +1,13 @@ + + + + Reset Your Password + + +

Hi,

+

You requested a password reset for your EveAI account. Click the link below to reset your password:

+

Reset Password

+

If you did not request a password reset, please ignore this email.

+

Thanks,
The EveAI Team

+ + \ No newline at end of file diff --git a/eveai_app/templates/security/email/reset_instructions.html b/eveai_app/templates/security/email/reset_instructions.html new file mode 100644 index 0000000..8ef3aab --- /dev/null +++ b/eveai_app/templates/security/email/reset_instructions.html @@ -0,0 +1,13 @@ + + + + Reset Your Password + + +

Hi,

+

You requested a password reset for your EveAI account. Click the link below to reset your password:

+

Reset Password

+

If you did not request a password reset, please ignore this email.

+

Thanks,
The EveAI Team

+ + \ No newline at end of file diff --git a/eveai_app/templates/security/forgot_password.html b/eveai_app/templates/security/forgot_password.html index eaa91dd..0fd4243 100644 --- a/eveai_app/templates/security/forgot_password.html +++ b/eveai_app/templates/security/forgot_password.html @@ -9,10 +9,16 @@ {% include "security/_messages.html" %}
{{ forgot_password_form.hidden_tag() }} + +

+ {{ forgot_password_form.email.label }}
+ {{ forgot_password_form.email(size=80) }} +

+

{{ forgot_password_form.submit() }}

+ {{ render_form_errors(forgot_password_form) }} - {{ render_field_with_errors(forgot_password_form.email) }} {{ render_field_errors(forgot_password_form.csrf_token) }} - {{ render_field(forgot_password_form.submit) }} +
- {% include "security/_menu.html" %} + {% endblock content %} diff --git a/eveai_app/templates/security/login_user.html b/eveai_app/templates/security/login_user.html index cf9e117..6e08bf3 100644 --- a/eveai_app/templates/security/login_user.html +++ b/eveai_app/templates/security/login_user.html @@ -14,10 +14,7 @@ {{ login_user_form.password.label }}
{{ login_user_form.password(size=80) }}

-{#

#} -{# {{ login_user_form.remember_me }}#} -{# {{ login_user_form.remember_me.label }}#} -{#

#} +

{{ login_user_form.submit() }}

diff --git a/eveai_app/templates/security/reset_password.html b/eveai_app/templates/security/reset_password.html index e8c59f2..5d89d04 100644 --- a/eveai_app/templates/security/reset_password.html +++ b/eveai_app/templates/security/reset_password.html @@ -1,12 +1,12 @@ {% extends "security/base.html" %} {% from "macros.html" import render_field %} -{% block title %} {{ _fsdomain('Reset password') }} {% endblock %} -{% block content_title %} {{ _fsdomain('Reset password') }} {% endblock %} +{% block title %} {{ _fsdomain('Reset AskEveAI password') }} {% endblock %} +{% block content_title %} {{ _fsdomain('Reset AskEveAI password') }} {% endblock %} {% block content_description %}An email will be sent to you with instructions.{% endblock %} {% block content %} {# {% include "security/_messages.html" %}#} -{#
#} +{# #} {{ reset_password_form.hidden_tag() }} {# {{ render_form_errors(reset_password_form) }}#} diff --git a/eveai_app/templates/user/view_users.html b/eveai_app/templates/user/view_users.html index 232f45b..8e63af9 100644 --- a/eveai_app/templates/user/view_users.html +++ b/eveai_app/templates/user/view_users.html @@ -12,6 +12,7 @@
+
diff --git a/eveai_app/views/security_forms.py b/eveai_app/views/security_forms.py index 71190bb..527282e 100644 --- a/eveai_app/views/security_forms.py +++ b/eveai_app/views/security_forms.py @@ -2,6 +2,10 @@ 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): @@ -18,4 +22,4 @@ class RequestResetForm(FlaskForm): class ResetPasswordForm(FlaskForm): password = PasswordField('Password', validators=[DataRequired()]) confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) - submit = SubmitField('Reset Password') \ No newline at end of file + submit = SubmitField('Reset Password') diff --git a/eveai_app/views/security_views.py b/eveai_app/views/security_views.py index e0ae75f..9adfad7 100644 --- a/eveai_app/views/security_views.py +++ b/eveai_app/views/security_views.py @@ -164,3 +164,5 @@ def reset_password(token): + + diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index 296b49e..e7afa51 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -154,30 +154,20 @@ def user(): form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant if form.validate_on_submit(): current_app.logger.info(f"Adding User for tenant {session['tenant']['id']} ") - if form.password.data != form.confirm_password.data: - flash('Passwords do not match.', 'danger') - return render_template('user/user.html', form=form) - # Handle the required attributes - hashed_password = hash_password(form.password.data) new_user = User(user_name=form.user_name.data, email=form.email.data, - password=hashed_password, first_name=form.first_name.data, last_name=form.last_name.data, valid_to=form.valid_to.data, - tenant_id=form.tenant_id.data + tenant_id=form.tenant_id.data, + fs_uniquifier=uuid.uuid4().hex, ) timestamp = dt.now(tz.utc) new_user.created_at = timestamp new_user.updated_at = timestamp - # Handle the relations - tenant_id = request.form.get('tenant_id') - # the_tenant = Tenant.query.get(tenant_id) - # new_user.tenant = the_tenant - # Add roles for role_id in form.roles.data: the_role = Role.query.get(role_id) @@ -188,11 +178,17 @@ def user(): try: db.session.add(new_user) db.session.commit() - security.datastore.set_uniquifier() - send_confirmation_email(new_user) - current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database' - f'Confirmation email sent to {new_user.email}') - flash('User added successfully and confirmation email sent.', 'success') + # security.datastore.set_uniquifier(new_user) + try: + send_confirmation_email(new_user) + current_app.logger.debug(f'User {new_user.id} with name {new_user.user_name} added to database' + f'Confirmation email sent to {new_user.email}') + flash('User added successfully and confirmation email sent.', 'success') + except Exception as e: + current_app.logger.error(f'Failed to send confirmation email to {new_user.email}. Error: {str(e)}') + flash('User added successfully, but failed to send confirmation email. ' + 'Please contact the administrator.', 'warning') + return redirect(prefixed_url_for('user_bp.view_users')) except Exception as e: current_app.logger.error(f'Failed to add user with name {new_user.user_name}. Error: {str(e)}') @@ -315,6 +311,9 @@ def handle_user_action(): elif action == 'resend_confirmation_email': send_confirmation_email(user) flash(f'Confirmation email sent to {user.email}.', 'success') + elif action == 'send_password_reset_email': + send_reset_email(user) + flash(f'Password reset email sent to {user.email}.', 'success') elif action == 'reset_uniquifier': reset_uniquifier(user) flash(f'Uniquifier reset for {user.user_name}.', 'success') diff --git a/logs/eveai_app.log b/logs/eveai_app.log index 474547f..6e4b818 100644 --- a/logs/eveai_app.log +++ b/logs/eveai_app.log @@ -86,3 +86,6 @@ 2024-07-08 12:16:20,402 [DEBUG] eveai_app: CELERY_RESULT_BACKEND: redis://redis:6379/0 2024-07-08 12:16:20,421 [INFO] eveai_app: EveAI App Server Started Successfully 2024-07-08 12:16:20,421 [INFO] eveai_app: ------------------------------------------------------------------------------------------------- +2024-08-20 15:41:17,290 [INFO] eveai_app (eveai_app) [__init__:48 in create_app] [Thread: MainThread]: eveai_app starting up +2024-08-20 15:41:17,291 [DEBUG] eveai_app (eveai_app) [__init__:49 in create_app] [Thread: MainThread]: start config +2024-08-20 15:41:17,291 [DEBUG] eveai_app (eveai_app) [__init__:50 in create_app] [Thread: MainThread]: is found before a full line is reached. In order to know the number of characters in a line, please check the document and the context within the document (e.g. an image could limit the number of characters temporarily).\n- Paragraphs are to be stripped of newlines so they become easily readable.\n- Be careful of encoding of the text. Everything needs to be human readable.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of the processing of the complete input pdf content. Answer with the pure markdown, without any other text.\n\nPDF content is between triple backquotes.\n\n```{pdf_content}```\n', 'summary': 'Write a concise summary of the text in {language}. The text is delimited between triple backquotes.\n```{text}```\n', 'rag': 'Answer the question based on the following context, delimited between triple backquotes. \n{tenant_context}\nUse the following {language} in your communication, and cite the sources used.\nIf the question cannot be answered using the given context, say "I have insufficient information to answer this question."\nContext:\n```{context}```\nQuestion:\n{question}\n', 'history': 'You are a helpful assistant that details a question based on a previous context,\nin such a way that the question is understandable without the previous context. \nThe context is a conversation history, with the HUMAN asking questions, the AI answering questions.\nThe history is delimited between triple backquotes.\nYou answer by stating the question in {language}.\nHistory:\n```{history}```\nQuestion to be detailed:\n{question}\n', 'encyclopedia': "You have a lot of background knowledge, and as such you are some kind of \n'encyclopedia' to explain general terminology. Only answer if you have a clear understanding of the question. \nIf not, say you do not have sufficient information to answer the question. Use the {language} in your communication.\nQuestion:\n{question}\n", 'transcript': 'You are a top administrative assistant specialized in transforming given transcriptions into markdown formatted files. The generated files will be used to generate embeddings in a RAG-system. The transcriptions originate from podcast, videos and similar material.\n\n# Best practices and steps are:\n- Respect wordings and language(s) used in the transcription. Main language is {language}.\n- Sometimes, the transcript contains speech of several people participating in a conversation. Although these are not obvious from reading the file, try to detect when other people are speaking. \n- Divide the transcript into several logical parts. Ensure questions and their answers are in the same logical part.\n- annotate the text to identify these logical parts using headings in {language}.\n- improve errors in the transcript given the context, but do not change the meaning and intentions of the transcription.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of processing the complete input transcription. Answer with the pure markdown, without any other text.\n\nThe transcript is between triple backquotes.\n\n```{transcript}```'}, 'anthropic.claude-3-5-sonnet': {'html_parse': 'You are a top administrative assistant specialized in transforming given HTML into markdown formatted files. The generated files will be used to generate embeddings in a RAG-system.\n\n# Best practices are:\n- Respect wordings and language(s) used in the HTML.\n- The following items need to be considered: headings, paragraphs, listed items (numbered or not) and tables. Images can be neglected.\n- Sub-headers can be used as lists. This is true when a header is followed by a series of sub-headers without content (paragraphs or listed items). Present those sub-headers as a list. \n- Be careful of encoding of the text. Everything needs to be human readable.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of the processing of the complete input html file. Answer with the pure markdown, without any other text.\n\nHTML is between triple backticks.\n\n```{html}``` \n', 'pdf_parse': 'You are a top administrative aid specialized in transforming given PDF-files into markdown formatted files. The generated files will be used to generate embeddings in a RAG-system.\n\n# Best practices are:\n- Respect wordings and language(s) used in the PDF.\n- The following items need to be considered: headings, paragraphs, listed items (numbered or not) and tables. Images can be neglected.\n- When headings are numbered, show the numbering and define the header level. \n- A new item is started when a is found before a full line is reached. In order to know the number of characters in a line, please check the document and the context within the document (e.g. an image could limit the number of characters temporarily).\n- Paragraphs are to be stripped of newlines so they become easily readable.\n- Be careful of encoding of the text. Everything needs to be human readable.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of the processing of the complete input pdf content. Answer with the pure markdown, without any other text.\n\nPDF content is between triple backticks.\n\n```{pdf_content}```\n', 'summary': 'Write a concise summary of the text in {language}. The text is delimited between triple backticks.\n```{text}```\n', 'rag': 'Answer the question based on the following context, delimited between triple backticks. \n{tenant_context}\nUse the following {language} in your communication, and cite the sources used.\nIf the question cannot be answered using the given context, say "I have insufficient information to answer this question."\nContext:\n```{context}```\nQuestion:\n{question}\n', 'history': 'You are a helpful assistant that details a question based on a previous context,\nin such a way that the question is understandable without the previous context. \nThe context is a conversation history, with the HUMAN asking questions, the AI answering questions.\nThe history is delimited between triple backticks.\nYou answer by stating the question in {language}.\nHistory:\n```{history}```\nQuestion to be detailed:\n{question}\n', 'encyclopedia': "You have a lot of background knowledge, and as such you are some kind of \n'encyclopedia' to explain general terminology. Only answer if you have a clear understanding of the question. \nIf not, say you do not have sufficient information to answer the question. Use the {language} in your communication.\nQuestion:\n{question}\n", 'transcript': '"""You are a top administrative assistant specialized in transforming given transcriptions into markdown formatted files. Your task is to process and improve the given transcript, not to summarize it.\n\nIMPORTANT INSTRUCTIONS:\n1. DO NOT summarize the transcript and don\'t make your own interpretations. Return the FULL, COMPLETE transcript with improvements.\n2. Improve any errors in the transcript based on context.\n3. Respect the original wording and language(s) used in the transcription. Main Language used is {language}.\n4. Divide the transcript into paragraphs for better readability. Each paragraph ONLY contains ORIGINAL TEXT.\n5. Group related paragraphs into logical sections.\n6. Add appropriate headers (using markdown syntax) to each section in {language}.\n7. We do not need an overall title. Just add logical headers\n8. Ensure that the entire transcript is included in your response, from start to finish.\n\nREMEMBER: \n- Your output should be the complete transcript in markdown format, NOT A SUMMARY OR ANALYSIS. \n- Include EVERYTHING from the original transcript, just organized and formatted better.\n- Just return the markdown version of the transcript, without any other text such as an introduction or a summary.\n\nHere is the transcript to process (between triple backticks):\n\n```{transcript}```\n\nProcess this transcript according to the instructions above and return the full, formatted markdown version.\n"""'}, 'openai.gpt-4o-mini': {'html_parse': 'You are a top administrative assistant specialized in transforming given HTML into markdown formatted files. The generated files will be used to generate embeddings in a RAG-system.\n\n# Best practices are:\n- Respect wordings and language(s) used in the HTML.\n- The following items need to be considered: headings, paragraphs, listed items (numbered or not) and tables. Images can be neglected.\n- Sub-headers can be used as lists. This is true when a header is followed by a series of sub-headers without content (paragraphs or listed items). Present those sub-headers as a list. \n- Be careful of encoding of the text. Everything needs to be human readable.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of the processing of the complete input html file. Answer with the pure markdown, without any other text.\n\nHTML is between triple backquotes.\n\n```{html}``` \n', 'pdf_parse': 'You are a top administrative aid specialized in transforming given PDF-files into markdown formatted files. The generated files will be used to generate embeddings in a RAG-system.\n\n# Best practices are:\n- Respect wordings and language(s) used in the PDF.\n- The following items need to be considered: headings, paragraphs, listed items (numbered or not) and tables. Images can be neglected.\n- When headings are numbered, show the numbering and define the header level. \n- A new item is started when a is found before a full line is reached. In order to know the number of characters in a line, please check the document and the context within the document (e.g. an image could limit the number of characters temporarily).\n- Paragraphs are to be stripped of newlines so they become easily readable.\n- Be careful of encoding of the text. Everything needs to be human readable.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of the processing of the complete input pdf content. Answer with the pure markdown, without any other text.\n\nPDF content is between triple backquotes.\n\n```{pdf_content}```\n', 'summary': 'Write a concise summary of the text in {language}. The text is delimited between triple backquotes.\n```{text}```\n', 'rag': 'Answer the question based on the following context, delimited between triple backquotes. \n{tenant_context}\nUse the following {language} in your communication, and cite the sources used.\nIf the question cannot be answered using the given context, say "I have insufficient information to answer this question."\nContext:\n```{context}```\nQuestion:\n{question}\n', 'history': 'You are a helpful assistant that details a question based on a previous context,\nin such a way that the question is understandable without the previous context. \nThe context is a conversation history, with the HUMAN asking questions, the AI answering questions.\nThe history is delimited between triple backquotes.\nYou answer by stating the question in {language}.\nHistory:\n```{history}```\nQuestion to be detailed:\n{question}\n', 'encyclopedia': "You have a lot of background knowledge, and as such you are some kind of \n'encyclopedia' to explain general terminology. Only answer if you have a clear understanding of the question. \nIf not, say you do not have sufficient information to answer the question. Use the {language} in your communication.\nQuestion:\n{question}\n", 'transcript': 'You are a top administrative assistant specialized in transforming given transcriptions into markdown formatted files. The generated files will be used to generate embeddings in a RAG-system. The transcriptions originate from podcast, videos and similar material.\n\n# Best practices and steps are:\n- Respect wordings and language(s) used in the transcription. Main language is {language}.\n- Sometimes, the transcript contains speech of several people participating in a conversation. Although these are not obvious from reading the file, try to detect when other people are speaking. \n- Divide the transcript into several logical parts. Ensure questions and their answers are in the same logical part.\n- annotate the text to identify these logical parts using headings in {language}.\n- improve errors in the transcript given the context, but do not change the meaning and intentions of the transcription.\n\nProcess the file carefully, and take a stepped approach. The resulting markdown should be the result of processing the complete input transcription. Answer with the pure markdown, without any other text.\n\nThe transcript is between triple backquotes.\n\n```{transcript}```'}}, 'REMEMBER_COOKIE_SAMESITE': 'strict', 'SECURITY_CONFIRMABLE': True, 'SECURITY_CSRF_COOKIE_NAME': 'XSRF-TOKEN', 'SECURITY_CSRF_HEADER': 'X-XSRF-TOKEN', 'SECURITY_CSRF_PROTECT_MECHANISMS': ['session'], 'SECURITY_EMAIL_SENDER': 'eveai_super@flow-it.net', 'SECURITY_LOGIN_URL': '/admin/login', 'SECURITY_LOGOUT_URL': '/admin/logout', 'SECURITY_PASSWORD_COMPLEXITY_CHECKER': 'zxcvbn', 'SECURITY_PASSWORD_SALT': None, 'SECURITY_POST_LOGIN_VIEW': '/user/tenant_overview', 'SECURITY_RECOVERABLE': True, 'SECURITY_TRACKABLE': True, 'SESSION_KEY_PREFIX': 'eveai_app_', 'SESSION_PERMANENT': True, 'SESSION_REDIS': )>)>, 'SESSION_TYPE': 'redis', 'SESSION_USE_SIGNER': True, 'SOCKETIO_ASYNC_MODE': 'gevent', 'SOCKETIO_CORS_ALLOWED_ORIGINS': '*', 'SOCKETIO_ENGINEIO_LOGGER': True, 'SOCKETIO_LOGGER': True, 'SOCKETIO_MAX_IDLE_TIME': datetime.timedelta(seconds=3600), 'SOCKETIO_MESSAGE_QUEUE': 'redis://redis:6379/1', 'SOCKETIO_PING_INTERVAL': 25000, 'SOCKETIO_PING_TIMEOUT': 20000, 'SQLALCHEMY_BINDS': {'public': 'postgresql+pg8000://luke:Skywalker!@localhost:5432/eveai'}, 'SQLALCHEMY_DATABASE_URI': 'postgresql+pg8000://luke:Skywalker!@localhost:5432/eveai', 'SUPPORTED_EMBEDDINGS': ['openai.text-embedding-3-small', 'openai.text-embedding-3-large', 'mistral.mistral-embed'], 'SUPPORTED_LANGUAGES': ['en', 'fr', 'nl', 'de', 'es'], 'SUPPORTED_LLMS': ['openai.gpt-4o', 'anthropic.claude-3-5-sonnet', 'openai.gpt-4o-mini'], 'UPLOAD_EXTENSIONS': ['.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif'], 'WTF_CSRF_CHECK_DEFAULT': False, 'WTF_CSRF_ENABLED': True, 'WTF_CSRF_SSL_STRICT': False, 'WTF_CSRF_TIME_LIMIT': None}> diff --git a/migrations/public/versions/229774547fed_fs_uniquifier_required_password_not_.py b/migrations/public/versions/229774547fed_fs_uniquifier_required_password_not_.py new file mode 100644 index 0000000..a928a04 --- /dev/null +++ b/migrations/public/versions/229774547fed_fs_uniquifier_required_password_not_.py @@ -0,0 +1,74 @@ +"""fs_uniquifier required / password not required for user + +Revision ID: 229774547fed +Revises: a39d2e378ccf +Create Date: 2024-08-21 08:49:55.397338 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '229774547fed' +down_revision = 'a39d2e378ccf' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('roles_users', schema=None) as batch_op: + batch_op.drop_constraint('roles_users_user_id_fkey', type_='foreignkey') + batch_op.drop_constraint('roles_users_role_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'user', ['user_id'], ['id'], referent_schema='public', ondelete='CASCADE') + batch_op.create_foreign_key(None, 'role', ['role_id'], ['id'], referent_schema='public', ondelete='CASCADE') + + with op.batch_alter_table('tenant_domain', schema=None) as batch_op: + batch_op.drop_constraint('tenant_domain_tenant_id_fkey', type_='foreignkey') + batch_op.drop_constraint('tenant_domain_updated_by_fkey', type_='foreignkey') + batch_op.drop_constraint('tenant_domain_created_by_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'user', ['updated_by'], ['id'], referent_schema='public') + batch_op.create_foreign_key(None, 'user', ['created_by'], ['id'], referent_schema='public') + batch_op.create_foreign_key(None, 'tenant', ['tenant_id'], ['id'], referent_schema='public') + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('password', + existing_type=sa.VARCHAR(length=255), + nullable=True) + batch_op.alter_column('fs_uniquifier', + existing_type=sa.VARCHAR(length=255), + nullable=False) + batch_op.drop_constraint('user_tenant_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'tenant', ['tenant_id'], ['id'], referent_schema='public') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('user_tenant_id_fkey', 'tenant', ['tenant_id'], ['id']) + batch_op.alter_column('fs_uniquifier', + existing_type=sa.VARCHAR(length=255), + nullable=True) + batch_op.alter_column('password', + existing_type=sa.VARCHAR(length=255), + nullable=False) + + with op.batch_alter_table('tenant_domain', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('tenant_domain_created_by_fkey', 'user', ['created_by'], ['id']) + batch_op.create_foreign_key('tenant_domain_updated_by_fkey', 'user', ['updated_by'], ['id']) + batch_op.create_foreign_key('tenant_domain_tenant_id_fkey', 'tenant', ['tenant_id'], ['id']) + + with op.batch_alter_table('roles_users', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('roles_users_role_id_fkey', 'role', ['role_id'], ['id'], ondelete='CASCADE') + batch_op.create_foreign_key('roles_users_user_id_fkey', 'user', ['user_id'], ['id'], ondelete='CASCADE') + + # ### end Alembic commands ### diff --git a/migrations/public/versions/a39d2e378ccf_uniquifier_not_required_when_creating_.py b/migrations/public/versions/a39d2e378ccf_uniquifier_not_required_when_creating_.py new file mode 100644 index 0000000..6d2a6fe --- /dev/null +++ b/migrations/public/versions/a39d2e378ccf_uniquifier_not_required_when_creating_.py @@ -0,0 +1,68 @@ +"""Uniquifier not required when creating user + +Revision ID: a39d2e378ccf +Revises: 1716099b62f0 +Create Date: 2024-08-20 15:53:08.692690 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a39d2e378ccf' +down_revision = '1716099b62f0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('roles_users', schema=None) as batch_op: + batch_op.drop_constraint('roles_users_role_id_fkey', type_='foreignkey') + batch_op.drop_constraint('roles_users_user_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'role', ['role_id'], ['id'], referent_schema='public', ondelete='CASCADE') + batch_op.create_foreign_key(None, 'user', ['user_id'], ['id'], referent_schema='public', ondelete='CASCADE') + + with op.batch_alter_table('tenant_domain', schema=None) as batch_op: + batch_op.drop_constraint('tenant_domain_updated_by_fkey', type_='foreignkey') + batch_op.drop_constraint('tenant_domain_created_by_fkey', type_='foreignkey') + batch_op.drop_constraint('tenant_domain_tenant_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'user', ['updated_by'], ['id'], referent_schema='public') + batch_op.create_foreign_key(None, 'user', ['created_by'], ['id'], referent_schema='public') + batch_op.create_foreign_key(None, 'tenant', ['tenant_id'], ['id'], referent_schema='public') + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('fs_uniquifier', + existing_type=sa.VARCHAR(length=255), + nullable=True) + batch_op.drop_constraint('user_tenant_id_fkey', type_='foreignkey') + batch_op.create_foreign_key(None, 'tenant', ['tenant_id'], ['id'], referent_schema='public') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('user_tenant_id_fkey', 'tenant', ['tenant_id'], ['id']) + batch_op.alter_column('fs_uniquifier', + existing_type=sa.VARCHAR(length=255), + nullable=False) + + with op.batch_alter_table('tenant_domain', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('tenant_domain_tenant_id_fkey', 'tenant', ['tenant_id'], ['id']) + batch_op.create_foreign_key('tenant_domain_created_by_fkey', 'user', ['created_by'], ['id']) + batch_op.create_foreign_key('tenant_domain_updated_by_fkey', 'user', ['updated_by'], ['id']) + + with op.batch_alter_table('roles_users', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('roles_users_user_id_fkey', 'user', ['user_id'], ['id'], ondelete='CASCADE') + batch_op.create_foreign_key('roles_users_role_id_fkey', 'role', ['role_id'], ['id'], ondelete='CASCADE') + + # ### end Alembic commands ### diff --git a/nginx/nginx.conf b/nginx/nginx.conf index c28dfe0..7583953 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -53,6 +53,10 @@ http { index index.html index.htm; } + location /reset { + rewrite ^/reset(.*)$ /admin/reset$1 permanent; + } + location /static/ { alias /etc/nginx/static/; }