diff --git a/common/models/user.py b/common/models/user.py index 9a2b78c..083d358 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -144,9 +144,9 @@ class TenantDomain(db.Model): # Versioning Information created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now()) - created_by = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) + created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=False) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) - updated_by = db.Column(db.Integer, db.ForeignKey(User.id)) + updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) def __repr__(self): return f"" @@ -202,13 +202,25 @@ class Partner(db.Model): tenant = db.relationship('Tenant', backref=db.backref('partner', uselist=False)) def to_dict(self): + services_info = [] + for service in self.services: + services_info.append({ + 'id': service.id, + 'name': service.name, + 'description': service.description, + 'type': service.type, + 'type_version': service.type_version, + 'active': service.active, + 'configuration': service.configuration + }) return { 'id': self.id, 'tenant_id': self.tenant_id, 'code': self.code, 'logo_url': self.logo_url, 'active': self.active, - 'name': self.tenant.name + 'name': self.tenant.name, + 'services': services_info } diff --git a/common/services/__init__.py b/common/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/services/user_service.py b/common/services/user_service.py new file mode 100644 index 0000000..beba4c3 --- /dev/null +++ b/common/services/user_service.py @@ -0,0 +1,44 @@ +from flask import session + +from common.models.user import Partner, Role + +# common/services/user_service.py +from common.utils.eveai_exceptions import EveAIRoleAssignmentException +from common.utils.security_utils import current_user_has_role, all_user_roles + + +class UserService: + @staticmethod + def get_assignable_roles(): + """Retrieves roles that can be assigned to a user depending on the current user logged in, + and the active tenant for the session""" + current_tenant_id = session.get('tenant').get('id', None) + effective_role_names = [] + if current_tenant_id: + if current_user_has_role("Super User"): + if current_tenant_id == 1: + effective_role_names.append("Super User") + if session.get('partner'): + effective_role_names.append("Partner Admin") + effective_role_names.append("Tenant Admin") + if current_user_has_role("Tenant Admin"): + effective_role_names.append("Tenant Admin") + if current_user_has_role("Partner Admin"): + effective_role_names.append("Tenant Admin") + if session.get('partner'): + if session.get('partner').get('tenant_id') == current_tenant_id: + effective_role_names.append("Partner Admin") + effective_role_names = list(set(effective_role_names)) + effective_roles = [(role.id, role.name) for role in + Role.query.filter(Role.name.in_(effective_role_names)).all()] + return effective_roles + else: + return [] + + @staticmethod + def validate_role_assignments(role_ids): + """Validate a set of role assignments, raising exception for first invalid role""" + assignable_roles = UserService.get_assignable_roles() + assignable_role_ids = {role[0] for role in assignable_roles} + role_id_set = set(role_ids) + return role_id_set.issubset(assignable_role_ids) diff --git a/common/utils/eveai_exceptions.py b/common/utils/eveai_exceptions.py index f24f42c..77eab11 100644 --- a/common/utils/eveai_exceptions.py +++ b/common/utils/eveai_exceptions.py @@ -147,3 +147,10 @@ class EveAIDoublePartner(EveAIException): message = f"Tenant with ID '{tenant_id}' is already defined as a Partner." super().__init__(message, status_code, payload) + +class EveAIRoleAssignmentException(EveAIException): + """Exception raised when a role cannot be assigned due to business rules""" + + def __init__(self, message, status_code=403, payload=None): + super().__init__(message, status_code, payload) + diff --git a/common/utils/security.py b/common/utils/security.py index 8e47912..6d4c723 100644 --- a/common/utils/security.py +++ b/common/utils/security.py @@ -1,7 +1,7 @@ from flask import session, current_app from sqlalchemy import and_ -from common.models.user import Tenant +from common.models.user import Tenant, Partner from common.models.entitlements import License from common.utils.database import Database from common.utils.eveai_exceptions import EveAITenantNotFound, EveAITenantInvalid, EveAINoActiveLicense @@ -13,13 +13,19 @@ def set_tenant_session_data(sender, user, **kwargs): tenant = Tenant.query.filter_by(id=user.tenant_id).first() session['tenant'] = tenant.to_dict() session['default_language'] = tenant.default_language - session['default_llm_model'] = tenant.llm_model + partner = Partner.query.filter_by(tenant_id=user.tenant_id).first() + if partner: + session['partner'] = partner.to_dict() + else: + # Remove partner from session if it exists + session.pop('partner', None) def clear_tenant_session_data(sender, user, **kwargs): session.pop('tenant', None) session.pop('default_language', None) session.pop('default_llm_model', None) + session.pop('partner', None) def is_valid_tenant(tenant_id): @@ -40,4 +46,4 @@ def is_valid_tenant(tenant_id): if not active_license: raise EveAINoActiveLicense(tenant_id) - return True \ No newline at end of file + return True diff --git a/common/utils/security_utils.py b/common/utils/security_utils.py index ef43e70..46f90cb 100644 --- a/common/utils/security_utils.py +++ b/common/utils/security_utils.py @@ -1,8 +1,10 @@ from flask import current_app, render_template +from flask_security import current_user from flask_mailman import EmailMessage from itsdangerous import URLSafeTimedSerializer import socket +from common.models.user import Role from common.utils.nginx_utils import prefixed_url_for @@ -93,3 +95,44 @@ def test_smtp_connection(): except Exception as e: current_app.logger.error(f"Failed to connect to SMTP server: {str(e)}") return False + + +def get_current_user_roles(): + """Get the roles of the currently authenticated user. + + Returns: + List of Role objects or empty list if no user is authenticated + """ + if current_user.is_authenticated: + return current_user.roles + return [] + + +def current_user_has_role(role_name): + """Check if the current user has the specified role. + + Args: + role_name (str): Name of the role to check + + Returns: + bool: True if user has the role, False otherwise + """ + if not current_user.is_authenticated: + return False + + return any(role.name == role_name for role in current_user.roles) + + +def current_user_roles(): + """Get the roles of the currently authenticated user. + + Returns: + List of Role objects or empty list if no user is authenticated + """ + if current_user.is_authenticated: + return current_user.roles + return [] + + +def all_user_roles(): + roles = [(role.id, role.name) for role in Role.query.all()] diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index c7b8d76..0a65eac 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -87,16 +87,6 @@ def create_app(config_file=None): sqlalchemy_logger.setLevel(logging.DEBUG) # log_request_middleware(app) # Add this when debugging nginx or another proxy - # Some generic Error Handling Routines - @app.errorhandler(Exception) - def handle_exception(e): - app.logger.error(f"Unhandled Exception: {e}", exc_info=True) - response = { - "message": str(e), - "type": type(e).__name__ - } - return jsonify(response), 500 - # @app.before_request # def before_request(): # # app.logger.debug(f"Before request - Session ID: {session.sid}") diff --git a/eveai_app/errors.py b/eveai_app/errors.py index 5ce2f6b..e2e77f4 100644 --- a/eveai_app/errors.py +++ b/eveai_app/errors.py @@ -1,5 +1,7 @@ +import traceback + import jinja2 -from flask import render_template, request, jsonify, redirect, current_app +from flask import render_template, request, jsonify, redirect, current_app, flash from flask_login import current_user from common.utils.nginx_utils import prefixed_url_for @@ -41,12 +43,46 @@ def key_error_handler(error): return render_template('error/generic.html', error_message="An unexpected error occurred"), 500 +def attribute_error_handler(error): + """Handle AttributeError exceptions. + + Specifically catches SQLAlchemy relationship errors when string IDs + are used instead of model instances. + """ + error_msg = str(error) + current_app.logger.error(f"AttributeError: {error_msg}") + current_app.logger.error(traceback.format_exc()) + + # Handle the SQLAlchemy relationship error specifically + if "'str' object has no attribute '_sa_instance_state'" in error_msg: + flash('Database relationship error. Please check your form inputs and try again.', 'error') + return render_template('errors/500.html', + error_type="Relationship Error", + error_details="A string value was provided where a database object was expected."), 500 + + # Handle other AttributeErrors + flash('An application error occurred. The technical team has been notified.', 'error') + return render_template('errors/500.html', + error_type="Attribute Error", + error_details=error_msg), 500 + + +def general_exception(e): + current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True) + flash('An application error occurred. The technical team has been notified.', 'error') + return render_template('errors/500.html', + error_type=type(e).__name__, + error_details=str(e)), 500 + + def register_error_handlers(app): app.register_error_handler(404, not_found_error) app.register_error_handler(500, internal_server_error) app.register_error_handler(401, not_authorised_error) app.register_error_handler(403, not_authorised_error) app.register_error_handler(KeyError, key_error_handler) + app.register_error_handler(AttributeError, attribute_error_handler) + app.register_error_handler(Exception, general_exception) @app.errorhandler(jinja2.TemplateNotFound) def template_not_found(error): diff --git a/eveai_app/templates/administration/partners.html b/eveai_app/templates/administration/partners.html index e9d1c94..ca8be31 100644 --- a/eveai_app/templates/administration/partners.html +++ b/eveai_app/templates/administration/partners.html @@ -16,6 +16,7 @@ + diff --git a/eveai_app/templates/administration/trigger_actions.html b/eveai_app/templates/administration/trigger_actions.html index 5829012..8e41619 100644 --- a/eveai_app/templates/administration/trigger_actions.html +++ b/eveai_app/templates/administration/trigger_actions.html @@ -8,7 +8,6 @@
-
diff --git a/eveai_app/templates/user/tenant.html b/eveai_app/templates/user/tenant.html index 5f80c59..77601a1 100644 --- a/eveai_app/templates/user/tenant.html +++ b/eveai_app/templates/user/tenant.html @@ -9,7 +9,7 @@ {% block content %}
{{ form.hidden_tag() }} - {% set disabled_fields = ['code'] %} + {% set disabled_fields = [] %} {% set exclude_fields = [] %} {% for field in form %} {{ render_field(field, disabled_fields, exclude_fields) }} diff --git a/eveai_app/views/administration_views.py b/eveai_app/views/administration_views.py index 2f19487..f0014d6 100644 --- a/eveai_app/views/administration_views.py +++ b/eveai_app/views/administration_views.py @@ -41,14 +41,6 @@ def handle_trigger_action(): except Exception as e: current_app.logger.error(f"Failed to trigger usage update task: {str(e)}") flash(f'Failed to trigger usage update: {str(e)}', 'danger') - case 'register_partner': - try: - partner_id = register_partner_from_tenant(session['tenant']['id']) - return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id, )) - except EveAIException as e: - current_app.logger.error(f'Error registering partner for tenant {session['tenant']['id']}: {str(e)}') - flash('Error Registering Partner for Selected Tenant', 'danger') - return redirect(prefixed_url_for('user_bp.select_tenant')) return redirect(prefixed_url_for('administration_bp.trigger_actions')) @@ -59,7 +51,8 @@ def edit_partner(partner_id): partner = Partner.query.get_or_404(partner_id) # This will return a 404 if no partner is found tenant = Tenant.query.get_or_404(partner.tenant_id) form = EditPartnerForm(obj=partner) - form.tenant.data = tenant.name + if request.method == 'GET': + form.tenant.data = tenant.name if form.validate_on_submit(): # Populate the user with form data @@ -107,6 +100,14 @@ def partners(): @roles_accepted('Super User') def handle_partner_selection(): action = request.form['action'] + if action == 'create_partner': + try: + partner_id = register_partner_from_tenant(session['tenant']['id']) + return redirect(prefixed_url_for('administration_bp.edit_partner', partner_id=partner_id, )) + except EveAIException as e: + current_app.logger.error(f'Error registering partner for tenant {session['tenant']['id']}: {str(e)}') + flash('Error Registering Partner for Selected Tenant', 'danger') + return redirect(prefixed_url_for('administration_bp.partners')) partner_identification = request.form.get('selected_row') partner_id = ast.literal_eval(partner_identification).get('value') partner = Partner.query.get_or_404(partner_id) diff --git a/eveai_app/views/user_forms.py b/eveai_app/views/user_forms.py index 44c780a..b282f79 100644 --- a/eveai_app/views/user_forms.py +++ b/eveai_app/views/user_forms.py @@ -6,6 +6,7 @@ from wtforms.validators import DataRequired, Length, Email, NumberRange, Optiona import pytz from common.models.user import Role +from common.services.user_service import UserService from config.type_defs.service_types import SERVICE_TYPES @@ -54,7 +55,7 @@ class BaseUserForm(FlaskForm): def __init__(self, *args, **kwargs): super(BaseUserForm, self).__init__(*args, **kwargs) - self.roles.choices = [(role.id, role.name) for role in Role.query.all()] + self.roles.choices = UserService.get_assignable_roles() class CreateUserForm(BaseUserForm): diff --git a/eveai_app/views/user_views.py b/eveai_app/views/user_views.py index 45295b7..95cb181 100644 --- a/eveai_app/views/user_views.py +++ b/eveai_app/views/user_views.py @@ -10,6 +10,7 @@ import ast from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, Partner from common.extensions import db, security, minio_client, simple_encryption +from common.services.user_service import UserService 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, \ @@ -106,7 +107,7 @@ def edit_tenant(tenant_id): @user_bp.route('/user', methods=['GET', 'POST']) -@roles_accepted('Super User', 'Tenant Admin') +@roles_accepted('Super User', 'Tenant Admin', 'Partner Admin') def user(): form = CreateUserForm() form.tenant_id.data = session.get('tenant').get('id') # It is only possible to create users for the session tenant @@ -159,7 +160,7 @@ def user(): @user_bp.route('/user/', methods=['GET', 'POST']) -@roles_accepted('Super User', 'Tenant Admin') +@roles_accepted('Super User', 'Tenant Admin', 'Partner Admin') def edit_user(user_id): user = User.query.get_or_404(user_id) # This will return a 404 if no user is found form = EditUserForm(obj=user) @@ -174,16 +175,22 @@ def edit_user(user_id): # Update roles current_roles = set(role.id for role in user.roles) selected_roles = set(form.roles.data) - # Add new roles - for role_id in selected_roles - current_roles: - role = Role.query.get(role_id) - if role: - user.roles.append(role) - # Remove unselected roles - for role_id in current_roles - selected_roles: - role = Role.query.get(role_id) - if role: - user.roles.remove(role) + if UserService.validate_role_assignments(selected_roles): + # Add new roles + for role_id in selected_roles - current_roles: + role = Role.query.get(role_id) + if role: + user.roles.append(role) + # Remove unselected roles + for role_id in current_roles - selected_roles: + role = Role.query.get(role_id) + if role: + user.roles.remove(role) + else: + flash('Trying to assign unauthorized roles', 'danger') + current_app.logger.error(f"Trying to assign unauthorized roles by user {user_id}," + f"tenant {session['tenant']['id']}") + return redirect(prefixed_url_for('user_bp.edit_user', user_id=user_id)) db.session.commit() flash('User updated successfully.', 'success') @@ -242,14 +249,10 @@ def handle_tenant_selection(): session.pop('catalog_name', None) match action: - case 'view_users': - return redirect(prefixed_url_for('user_bp.view_users', tenant_id=tenant_id)) case 'edit_tenant': return redirect(prefixed_url_for('user_bp.edit_tenant', tenant_id=tenant_id)) case 'select_tenant': return redirect(prefixed_url_for('user_bp.tenant_overview')) - case 'new_tenant': - return redirect(prefixed_url_for('user_bp.tenant')) # Add more conditions for other actions return redirect(prefixed_url_for('select_tenant')) diff --git a/migrations/public/versions/9ac89fc67661_sequence_changes.py b/migrations/public/versions/9ac89fc67661_sequence_changes.py new file mode 100644 index 0000000..4155505 --- /dev/null +++ b/migrations/public/versions/9ac89fc67661_sequence_changes.py @@ -0,0 +1,31 @@ +"""Sequence changes + +Revision ID: 9ac89fc67661 +Revises: 867deef0888b +Create Date: 2025-04-03 13:26:55.480553 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9ac89fc67661' +down_revision = '867deef0888b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + tables = ['role', 'tenant', 'user', 'partner', 'tenant_domain', 'tenant_project'] + for table in tables: + op.execute(f"ALTER SEQUENCE public.{table}_id_seq RESTART WITH 1000;") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/nginx/static/assets/css/eveai.css b/nginx/static/assets/css/eveai.css index 314f1b7..0fc2e1a 100644 --- a/nginx/static/assets/css/eveai.css +++ b/nginx/static/assets/css/eveai.css @@ -330,11 +330,20 @@ input[type="radio"] { color: var(--bs-body-color) !important; /* Text color consistent with the theme */ } -.form-control:disabled { - background-color: var(--bs-gray-100) !important; /* Gray background for disabled fields */ - color: var(--bs-gray-600) !important; /* Dimmed text color for disabled fields */ +/* Style for both disabled and readonly fields - same gray background */ +.form-control:disabled, +.form-control[readonly] { + background-color: var(--bs-gray-100) !important; /* Gray background */ + color: var(--bs-gray-600) !important; /* Dimmed text color */ } +/* Light orange background for editable fields */ +/* TODO +.form-control:not([readonly]):not(:disabled) { + background-color: #ffe4d6 !important; +} +*/ + .form-check-input:checked { background-color: var(--bs-primary) !important; /* Primary color for checked checkboxes */ border-color: var(--bs-primary) !important; /* Primary color for checkbox border */ diff --git a/scripts/initialize_data.py b/scripts/initialize_data.py index eef0a3e..d2378ff 100644 --- a/scripts/initialize_data.py +++ b/scripts/initialize_data.py @@ -1,81 +1,155 @@ +from sqlalchemy import text from sqlalchemy.exc import IntegrityError from datetime import datetime as dt, timezone as tz from flask_security import hash_password from uuid import uuid4 from eveai_app import create_app -from common.extensions import db, security from common.models.user import User, Tenant, Role, RolesUsers +from common.extensions import db, minio_client from common.utils.database import Database +from flask_security.utils import hash_password + def initialize_data(): - app = create_app() - with app.app_context(): - # Define Initial Tenant - if Tenant.query.first() is None: - print("No tenants found. Creating initial tenant...") - timestamp = dt.now(tz=tz.utc) - tenant = Tenant(name="Jedi", - website="https://askeveai.com", - created_at=timestamp, - updated_at=timestamp) - db.session.add(tenant) - db.session.commit() - else: - print("Tenants already exist. Skipping tenant creation.") + """ + Initialize baseline data for a new EveAI environment. + This function creates: + - System roles (IDs 1-999) + - Default tenant (ID 1) + - Admin user (ID 1) + """ + print("Starting data initialization...") + + app = create_app() + + with app.app_context(): + + # Step 1: Initialize roles + roles = initialize_roles() + + # Step 2: Initialize default tenant + default_tenant = initialize_default_tenant() + + # Step 3: Initialize admin user + admin_user = initialize_admin_user(default_tenant) + + print("Data initialization completed successfully") + + +def initialize_roles(): + """Initialize system roles with IDs below 1000""" + print("Initializing system roles...") + + # Define system roles - matching the exact IDs and names you specified + roles_data = [ + {'id': 1, 'name': 'Super User', 'description': 'System administrator with full access'}, + {'id': 2, 'name': 'Tenant Admin', 'description': 'Administrator for a specific tenant'}, + {'id': 3, 'name': 'Partner Admin', 'description': 'Partner Administrator, managing different Tenants'}, + ] + + roles = {} + for role_data in roles_data: + role = Role.query.filter_by(name=role_data['name']).first() + + if not role: + print(f"Creating role: {role_data['name']} (ID: {role_data['id']})") + role = Role(**role_data) + db.session.add(role) + else: + print(f"Role already exists: {role.name} (ID: {role.id})") + + roles[role_data['name']] = role + + db.session.commit() + + # Verify sequence is set correctly + db.session.execute(text("ALTER SEQUENCE public.role_id_seq RESTART WITH 1000")) + db.session.commit() + + return roles + + +def initialize_default_tenant(): + """Initialize the default system tenant with ID 1""" + print("Initializing default tenant...") + + tenant_data = { + 'id': 1, + 'name': 'Jedi', + 'website': 'https://www.askeveai.com', + 'timezone': 'UTC', + 'default_language': 'en', + 'allowed_languages': ['en', 'fr', 'nl', 'de', 'es'], + 'llm_model': 'mistral.mistral-large-latest', + 'type': 'Active', + 'currency': '€', + 'created_at': dt.now(tz.utc), + 'updated_at': dt.now(tz.utc) + } + + tenant = db.session.get(Tenant, tenant_data['id']) + + if not tenant: + print(f"Creating default tenant: {tenant_data['name']} (ID: {tenant_data['id']})") + tenant = Tenant(**tenant_data) + db.session.add(tenant) + db.session.commit() + + # Create tenant schema + print(f"Creating schema for tenant {tenant.id}") Database(tenant.id).create_tenant_schema() - # Define Roles - super_user_role = Role.query.filter_by(name="Super User").first() - if super_user_role is None: - super_user_role = Role(name="Super User", description="Users allowed to perform all functions") - db.session.add(super_user_role) - db.session.commit() - tenant_admin_role = Role.query.filter_by(name="Tenant Admin").first() - if tenant_admin_role is None: - tenant_admin_role = Role(name="Tenant Admin", description="Users allowed to manage tenants") - db.session.add(tenant_admin_role) - db.session.commit() - # tenant_tester_role = Role.query.filter_by(name="Tenant Tester").first() - # if tenant_tester_role is None: - # tenant_test_role = Role(name="Tenant Tester", description="Users allowed to test tenants") - # db.session.add(tenant_test_role) - # db.session.commit() + # Create MinIO bucket + print(f"Creating MinIO bucket for tenant {tenant.id}") + minio_client.create_tenant_bucket(tenant.id) + else: + print(f"Default tenant already exists: {tenant.name} (ID: {tenant.id})") - # Check if any users exist - if User.query.first() is None: - print("No users found. Creating initial user...") - # Ensure tenant exists before creating the user - tenant = Tenant.query.filter_by(name="Jedi").first() - if tenant: - user = User( - user_name="yoda", - email="yoda@flow-it.net", - password=hash_password("Dagobah"), - first_name="Yoda", - last_name="Skywalker", - tenant_id=tenant.id, - created_at=dt.now(tz=tz.utc), - updated_at=dt.now(tz=tz.utc), - fs_uniquifier=str(uuid4()), - active=True, - confirmed_at=dt.now(tz=tz.utc) - ) - db.session.add(user) - db.session.commit() - # security.datastore.set_uniquifier() - print("Initial user created.") - # Assign SuperUser role to the new user - user_role = RolesUsers(user_id=user.id, role_id=super_user_role.id) - db.session.add(user_role) - db.session.commit() - print("SuperUser role assigned to the new user.") - else: - print("Failed to find initial tenant for user creation.") - else: - print("Users already exist. Skipping user creation.") + return tenant + + +def initialize_admin_user(tenant): + """Initialize the system admin user with ID 1""" + print("Initializing admin user...") + + # Check if admin user already exists + admin_user = User.query.filter_by(email='yoda@flow-it.net').first() + + if not admin_user: + print("Creating admin user (yoda)") + + # Create a secure password - you can replace this with your preferred default + password = hash_password('Dagobah') + + # Create the admin user with ID 1 + admin_user = User( + id=1, + tenant_id=tenant.id, + user_name='yoda', + email='yoda@flow-it.net', + password=password, + first_name='Yoda', + last_name='Master', + active=True, + fs_uniquifier=str(uuid4()), + confirmed_at=dt.now(tz.utc), + created_at=dt.now(tz.utc), + updated_at=dt.now(tz.utc) + ) + + db.session.add(admin_user) + db.session.commit() + + user_role = RolesUsers(user_id=admin_user.id, role_id=1) + db.session.add(user_role) + db.session.commit() + else: + print(f"Admin user already exists: {admin_user.email} (ID: {admin_user.id})") + + return admin_user if __name__ == "__main__":