- Correctie reset password en confirm email adress by adapting the prefixed_url_for to use config setting

- Adaptation of DPA and T&Cs
- Refer to privacy statement as DPA, not a privacy statement
- Startup of enforcing signed DPA and T&Cs
- Adaptation of eveai_chat_client to ensure we retrieve correct DPA & T&Cs
This commit is contained in:
Josako
2025-10-13 14:28:09 +02:00
parent 83272a4e2a
commit 37819cd7e5
35 changed files with 5350 additions and 241 deletions

View File

@@ -70,6 +70,7 @@
{% if current_user.is_authenticated %}
{{ dropdown('Tenants', 'source_environment', [
{'name': 'Tenants', 'url': 'user/tenants', 'roles': ['Super User', 'Partner Admin']},
{'name': 'Consent Versions', 'url': 'user/consent_versions', 'roles': ['Super User']},
{'name': 'Tenant Overview', 'url': 'user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Edit Tenant', 'url': 'user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
{'name': 'Tenant Partner Services', 'url': 'user/tenant_partner_services', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},

View File

@@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field, render_included_field %}
{% block title %}Create or Edit Consent Version{% endblock %}
{% block content_title %}Create or Edit Consent Version{% endblock %}
{% block content_description %}Create or Edit Consent Version{% 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">Save Consent Version</button>
</form>
{% endblock %}
{% block content_footer %}
{% endblock %}
{% block scripts %}
<script>
// JavaScript om de gebruiker's timezone te detecteren
document.addEventListener('DOMContentLoaded', (event) => {
// Detect timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Send timezone to the server via a POST request
fetch('set_user_timezone', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ timezone: userTimezone })
}).then(response => {
if (response.ok) {
console.log('Timezone sent to server successfully');
} else {
console.error('Failed to send timezone to server');
}
});
$('#timezone').select2({
placeholder: 'Selecteer een timezone...',
allowClear: true,
theme: 'bootstrap',
width: '100%',
dropdownAutoWidth: true,
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
scrollAfterSelect: false,
// Verbeterd scroll gedrag
dropdownParent: $('body')
});
// Stel de huidige waarde in als de dropdown wordt geopend
$('#timezone').on('select2:open', function() {
if ($(this).val()) {
setTimeout(function() {
let selectedOption = $('.select2-results__option[aria-selected=true]');
if (selectedOption.length) {
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
}
}, 0);
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field, render_included_field %}
{% block title %}Create or Edit Consent Version{% endblock %}
{% block content_title %}Create or Edit Consent Version{% endblock %}
{% block content_description %}Create or Edit Consent Version{% endblock %}
{% block content %}
<form method="post">
{{ form.hidden_tag() }}
{% set disabled_fields = ["consent_type"] %}
{% set exclude_fields = [] %}
{% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
<button type="submit" class="btn btn-primary">Save Consent Version</button>
</form>
{% endblock %}
{% block content_footer %}
{% endblock %}
{% block scripts %}
<script>
// JavaScript om de gebruiker's timezone te detecteren
document.addEventListener('DOMContentLoaded', (event) => {
// Detect timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Send timezone to the server via a POST request
fetch('set_user_timezone', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ timezone: userTimezone })
}).then(response => {
if (response.ok) {
console.log('Timezone sent to server successfully');
} else {
console.error('Failed to send timezone to server');
}
});
$('#timezone').select2({
placeholder: 'Selecteer een timezone...',
allowClear: true,
theme: 'bootstrap',
width: '100%',
dropdownAutoWidth: true,
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
scrollAfterSelect: false,
// Verbeterd scroll gedrag
dropdownParent: $('body')
});
// Stel de huidige waarde in als de dropdown wordt geopend
$('#timezone').on('select2:open', function() {
if ($(this).val()) {
setTimeout(function() {
let selectedOption = $('.select2-results__option[aria-selected=true]');
if (selectedOption.length) {
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
}
}, 0);
}
});
});
</script>
{% endblock %}

View File

@@ -118,7 +118,7 @@ def view_content(content_type):
Show content like release notes, terms of use, etc.
Args:
content_type (str): Type content (eg. 'changelog', 'terms', 'privacy')
content_type (str): Type content (eg. 'changelog', 'terms', 'dpa')
"""
try:
major_minor = request.args.get('version')
@@ -135,14 +135,14 @@ def view_content(content_type):
titles = {
'changelog': 'Release Notes',
'terms': 'Terms & Conditions',
'privacy': 'Privacy Statement',
'dpadpa': 'Data Privacy Agreement',
# Voeg andere types toe indien nodig
}
descriptions = {
'changelog': 'EveAI Release Notes',
'terms': "Terms & Conditions for using AskEveAI's Evie",
'privacy': "Privacy Statement for AskEveAI's Evie",
'dpadpa': "Data Privacy Agreement for AskEveAI's Evie",
# Voeg andere types toe indien nodig
}

View File

@@ -3,7 +3,8 @@ from flask_security import roles_accepted
from sqlalchemy.exc import SQLAlchemyError
import ast
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService
from common.models.user import Tenant, User, TenantDomain, TenantProject, TenantMake, PartnerTenant, PartnerService, \
ConsentVersion
from common.services.user import UserServices, PartnerServices
from common.utils.eveai_exceptions import EveAINoSessionPartner, EveAINoManagementPartnerService
from common.utils.security_utils import current_user_has_role
@@ -287,6 +288,8 @@ def get_tenant_makes_list_view(tenant_id):
# Tenant Partner Services list view helper
def get_tenant_partner_services_list_view(tenant_id):
"""Generate the tenant partner services list view configuration for a specific tenant"""
# Get partner services for the tenant through PartnerTenant association
@@ -328,3 +331,48 @@ def get_tenant_partner_services_list_view(tenant_id):
'form_action': url_for('user_bp.tenant_partner_services'),
'description': f'Partner Services for tenant {tenant_id}'
}
def get_consent_versions_list_view():
"""Generate the tenant makes list view configuration for a specific tenant"""
# Get makes for the tenant
query = ConsentVersion.query.filter_by().order_by(ConsentVersion.id)
consent_versions = query.all()
# Prepare data for Tabulator
data = []
for cv in consent_versions:
data.append({
'id': cv.id,
'consent_type': cv.consent_type,
'consent_version': cv.consent_version,
'consent_valid_from': cv.consent_valid_from.strftime('%Y-%m-%d') if cv.consent_valid_from else '',
'consent_valid_to': cv.consent_valid_to.strftime('%Y-%m-%d') if cv.consent_valid_to else '',
})
# Column Definitions
columns = [
{'title': 'ID', 'field': 'id', 'width': 80},
{'title': 'Consent Type', 'field': 'consent_type'},
{'title': 'From', 'field': 'consent_valid_from'},
{'title': 'To', 'field': 'consent_valid_to'}
]
actions = [
{'value': 'edit_consent_version', 'text': 'Edit Consent Version', 'class': 'btn-primary', 'requiresSelection': True},
{'value': 'create_consent_version', 'text': 'Create Consent Version', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False},
]
initial_sort = [{'column': 'id', 'dir': 'asc'}]
return {
'title': 'Consent Versions',
'data': data,
'columns': columns,
'actions': actions,
'initial_sort': initial_sort,
'table_id': 'consent_versions_table',
'form_action': url_for('user_bp.handle_consent_version_selection'),
'description': f'Consent Versions'
}

View File

@@ -10,7 +10,8 @@ from datetime import datetime as dt, timezone as tz
from itsdangerous import URLSafeTimedSerializer
from sqlalchemy.exc import SQLAlchemyError
from common.models.user import User
from common.models.user import User, ConsentStatus
from common.services.user import TenantServices
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, ForgotPasswordForm
@@ -56,8 +57,24 @@ def login():
db.session.commit()
if current_user.has_roles('Super User'):
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
else:
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
if current_user.has_roles('Partner Admin'):
return redirect(prefixed_url_for('user_bp.tenants', for_redirect=True))
consent_status = TenantServices.get_consent_status(user.tenant_id)
match consent_status:
case ConsentStatus.CONSENTED:
return redirect(prefixed_url_for('user_bp.tenant_overview', for_redirect=True))
case ConsentStatus.NOT_CONSENTED:
if current_user.has_roles('Tenant Admin'):
return redirect(prefixed_url_for('user_bp.tenant_consent', for_redirect=True))
else:
return redirect(prefixed_url_for('user_bp.no_consent', for_redirect=True))
case ConsentStatus.RENEWAL_REQUIRED:
if current_user.has_roles('Tenant Admin'):
return redirect(prefixed_url_for('user_bp.tenant_consent_renewal', for_redirect=True))
else:
return redirect(prefixed_url_for('user_bp.consent_renewal', for_redirect=True))
case _:
return redirect(prefixed_url_for('basic_bp.index', for_redirect=True))
else:
flash('Invalid username or password', 'danger')
current_app.logger.error(f'Invalid username or password for given email: {user.email}')

View File

@@ -192,6 +192,7 @@ class TenantMakeForm(DynamicFormBase):
self.allowed_languages.choices = [(details['iso 639-1'], f"{details['flag']} {details['iso 639-1']}")
for name, details in lang_details.items()]
class EditTenantMakeForm(DynamicFormBase):
id = IntegerField('ID', widget=HiddenInput())
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
@@ -212,5 +213,22 @@ class EditTenantMakeForm(DynamicFormBase):
self.default_language.choices = choices
class ConsentVersionForm(FlaskForm):
consent_type = SelectField('Consent Type', choices=[], validators=[DataRequired()])
consent_version = StringField('Consent Version', validators=[DataRequired(), Length(max=20)])
consent_valid_from = DateField('Consent Valid From', id='form-control datepicker', validators=[DataRequired()])
consent_valid_to = DateField('Consent Valid To', id='form-control datepicker', validators=[Optional()])
def __init__(self, *args, **kwargs):
super(ConsentVersionForm, self).__init__(*args, **kwargs)
# Initialise consent types
self.consent_type.choices = [(t, t) for t in current_app.config['CONSENT_TYPES']]
class EditConsentVersionForm(FlaskForm):
consent_type = StringField('Consent Type', validators=[DataRequired()])
consent_version = StringField('Consent Version', validators=[DataRequired(), Length(max=20)])
consent_valid_from = DateField('Consent Valid From', id='form-control datepicker', validators=[DataRequired()])
consent_valid_to = DateField('Consent Valid To', id='form-control datepicker', validators=[Optional()])

View File

@@ -6,13 +6,15 @@ from flask_security import roles_accepted, current_user
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
import ast
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake, \
ConsentVersion
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
from common.utils.dynamic_field_utils import create_default_config_from_type_config
from common.utils.security_utils import send_confirmation_email, send_reset_email
from config.type_defs.service_types import SERVICE_TYPES
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm, ConsentVersionForm, \
EditConsentVersionForm
from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key
@@ -25,7 +27,7 @@ from common.utils.mail_utils import send_email
from eveai_app.views.list_views.user_list_views import get_tenants_list_view, get_users_list_view, \
get_tenant_domains_list_view, get_tenant_projects_list_view, get_tenant_makes_list_view, \
get_tenant_partner_services_list_view
get_tenant_partner_services_list_view, get_consent_versions_list_view
from eveai_app.views.list_views.list_view_utils import render_list_view
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -693,6 +695,87 @@ def tenant_partner_services():
return render_list_view('list_view.html', **config)
@user_bp.route('/consent_versions', methods=['GET', 'POST'])
@roles_accepted('Super User')
def consent_versions():
config = get_consent_versions_list_view()
return render_list_view('list_view.html', **config)
@user_bp.route('/handle_consent_version_selection', methods=['POST'])
@roles_accepted('Super User')
def handle_consent_version_selection():
action = request.form['action']
if action == 'create_consent_version':
return redirect(prefixed_url_for('user_bp.consent_version', for_redirect=True))
consent_version_identification = request.form.get('selected_row')
consent_version_id = ast.literal_eval(consent_version_identification).get('value')
if action == 'edit_consent_version':
return redirect(prefixed_url_for('user_bp.edit_consent_version', consent_version_id=consent_version_id, for_redirect=True))
# Altijd teruggaan naar de tenant_makes pagina
return redirect(prefixed_url_for('user_bp.consent_versions', for_redirect=True))
@user_bp.route('/consent_version', methods=['GET', 'POST'])
@roles_accepted('Super User')
def consent_version():
form = ConsentVersionForm()
if form.validate_on_submit():
new_consent_version = ConsentVersion()
form.populate_obj(new_consent_version)
set_logging_information(new_consent_version, dt.now(tz.utc))
try:
db.session.add(new_consent_version)
db.session.commit()
flash('Consent Version successfully added!', 'success')
current_app.logger.info(f'Consent Version {new_consent_version.consent_type}, version {new_consent_version.consent_version} successfully added ')
# Enable step 2 of creation of retriever - add configuration of the retriever (dependent on type)
return redirect(prefixed_url_for('user_bp.consent_versions', for_redirect=True))
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to add Consent Version. Error: {e}', 'danger')
current_app.logger.error(f'Failed to add Consent Version. Error: {str(e)}')
return render_template('user/consent_version.html', form=form)
@user_bp.route('/consent_version/<int:consent_version_id>', methods=['GET', 'POST'])
@roles_accepted('Super User')
def edit_consent_version(consent_version_id):
"""Edit an existing Consent Version."""
# Get the Consent Version or return 404
cv = ConsentVersion.query.get_or_404(consent_version_id)
# Create form instance with the tenant make
form = EditConsentVersionForm(request.form, obj=cv)
if form.validate_on_submit():
# Update basic fields
form.populate_obj(cv)
# Update logging information
update_logging_information(cv, dt.now(tz.utc))
# Save changes to database
try:
db.session.add(cv)
db.session.commit()
flash('Consent Version updated successfully!', 'success')
current_app.logger.info(f'Consent Version {cv.id} updated successfully')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to update Consent Version. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update Consent Version {consent_version_id}. Error: {str(e)}')
return render_template('user/consent_version.html', form=form, consent_version_id=consent_version_id)
return redirect(prefixed_url_for('user_bp.consent_versions', for_redirect=True))
else:
form_validation_failed(request, form)
return render_template('user/edit_consent_version.html', form=form, consent_version_id=consent_version_id)
def reset_uniquifier(user):
security.datastore.set_uniquifier(user)