Correct functions for creating new users, confirming email, resetting password and forgot password.

This commit is contained in:
Josako
2024-08-21 14:59:56 +02:00
parent 6219d11e56
commit af3609bb05
20 changed files with 291 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<title>Reset Your Password</title>
</head>
<body>
<p>Hi,</p>
<p>You requested a password reset for your EveAI account. Click the link below to reset your password:</p>
<p><a href="{{ reset_url }}">Reset Password</a></p>
<p>If you did not request a password reset, please ignore this email.</p>
<p>Thanks,<br>The EveAI Team</p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<title>Reset Your Password</title>
</head>
<body>
<p>Hi,</p>
<p>You requested a password reset for your EveAI account. Click the link below to reset your password:</p>
<p><a href="{{ reset_link }}">Reset Password</a></p>
<p>If you did not request a password reset, please ignore this email.</p>
<p>Thanks,<br>The EveAI Team</p>
</body>
</html>

View File

@@ -9,10 +9,16 @@
{% include "security/_messages.html" %}
<form action="{{ url_for_security('forgot_password') }}" method="post" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
<p>
{{ forgot_password_form.email.label }}<br>
{{ forgot_password_form.email(size=80) }}
</p>
<p>{{ forgot_password_form.submit() }}</p>
{{ 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) }}
</form>
{% include "security/_menu.html" %}
<!-- {% include "security/_menu.html" %}-->
{% endblock content %}

View File

@@ -14,10 +14,7 @@
{{ login_user_form.password.label }}<br>
{{ login_user_form.password(size=80) }}
</p>
{# <p>#}
{# {{ login_user_form.remember_me }}#}
{# {{ login_user_form.remember_me.label }}#}
{# </p>#}
<p>{{ login_user_form.submit() }}</p>
</form>

View File

@@ -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" %}#}
{# <form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="post" name="reset_password_form">#}
{# <form action="{{ prefixed_url_for('security.reset_password', token=reset_password_token) }}" method="post" name="reset_password_form">#}
<form action="" method="post">
{{ reset_password_form.hidden_tag() }}
{# {{ render_form_errors(reset_password_form) }}#}

View File

@@ -12,6 +12,7 @@
<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>

View File

@@ -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')
submit = SubmitField('Reset Password')

View File

@@ -164,3 +164,5 @@ def reset_password(token):

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -53,6 +53,10 @@ http {
index index.html index.htm;
}
location /reset {
rewrite ^/reset(.*)$ /admin/reset$1 permanent;
}
location /static/ {
alias /etc/nginx/static/;
}