diff --git a/common/models/user.py b/common/models/user.py index e88e31a..868343f 100644 --- a/common/models/user.py +++ b/common/models/user.py @@ -37,9 +37,11 @@ class Tenant(db.Model): license_start_date = db.Column(db.Date, nullable=True) license_end_date = db.Column(db.Date, nullable=True) allowed_monthly_interactions = db.Column(db.Integer, nullable=True) + encrypted_api_key = db.Column(db.String(500), nullable=True) # Relations users = db.relationship('User', backref='tenant') + domains = db.relationship('TenantDomain', backref='tenant') def __repr__(self): return f"" @@ -115,3 +117,23 @@ class User(db.Model, UserMixin): def has_roles(self, *args): return any(role.name in args for role in self.roles) + + +class TenantDomain(db.Model): + __bind_key__ = 'public' + __table_args__ = {'schema': 'public'} + + id = db.Column(db.Integer, primary_key=True) + tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False) + domain = db.Column(db.String(255), unique=True, nullable=False) + valid_to = db.Column(db.Date, nullable=True) + + # 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) + 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)) + + def __repr__(self): + return f"" + diff --git a/common/utils/key_encryption.py b/common/utils/key_encryption.py new file mode 100644 index 0000000..060a2b4 --- /dev/null +++ b/common/utils/key_encryption.py @@ -0,0 +1,51 @@ +from google.cloud import kms +from base64 import b64encode, b64decode +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from flask import current_app + +client = kms.KeyManagementServiceClient() +key_name = client.crypto_key_path('your-project-id', 'your-key-ring', 'your-crypto-key') + + +def encrypt_api_key(api_key): + """Encrypts the API key using the latest version of the KEK.""" + dek = get_random_bytes(32) # AES 256-bit key + cipher = AES.new(dek, AES.MODE_GCM) + ciphertext, tag = cipher.encrypt_and_digest(api_key.encode()) + + # Encrypt the DEK using the latest version of the Google Cloud KMS key + encrypt_response = client.encrypt( + request={'name': key_name, 'plaintext': dek} + ) + encrypted_dek = encrypt_response.ciphertext + + # Store the version of the key used + key_version = encrypt_response.name + + return { + 'key_version': key_version, + 'encrypted_dek': b64encode(encrypted_dek).decode('utf-8'), + 'nonce': b64encode(cipher.nonce).decode('utf-8'), + 'tag': b64encode(tag).decode('utf-8'), + 'ciphertext': b64encode(ciphertext).decode('utf-8') + } + + +def decrypt_api_key(encrypted_data): + """Decrypts the API key using the specified key version.""" + key_version = encrypted_data['key_version'] + encrypted_dek = b64decode(encrypted_data['encrypted_dek']) + nonce = b64decode(encrypted_data['nonce']) + tag = b64decode(encrypted_data['tag']) + ciphertext = b64decode(encrypted_data['ciphertext']) + + # Decrypt the DEK using the specified version of the Google Cloud KMS key + decrypt_response = client.decrypt( + request={'name': key_version, 'ciphertext': encrypted_dek} + ) + dek = decrypt_response.plaintext + + cipher = AES.new(dek, AES.MODE_GCM, nonce=nonce) + api_key = cipher.decrypt_and_verify(ciphertext, tag) + return api_key.decode() diff --git a/common/utils/view_assistants.py b/common/utils/view_assistants.py new file mode 100644 index 0000000..a7ea7aa --- /dev/null +++ b/common/utils/view_assistants.py @@ -0,0 +1,36 @@ +def prepare_table(model_objects, column_names): + """ + Converts a list of SQLAlchemy model objects into a list of dictionaries based on specified column names. + + Args: + model_objects (list): List of SQLAlchemy model instances. + column_names (list): List of strings representing the column names to be included in the dictionaries. + + Returns: + list: List of dictionaries where each dictionary represents a record with keys as column names. + """ + table_data = [ + {col: getattr(obj, col) for col in column_names} + for obj in model_objects + ] + return table_data + + +def prepare_table_for_macro(model_objects, column_attrs): + """ + Prepare data for rendering in a macro that expects each cell as a dictionary with class, type, and value. + + Args: + model_objects (list): List of model instances or dictionaries. + column_attrs (list of tuples): Each tuple contains the attribute name and additional properties like class. + + Returns: + list: A list of rows, where each row is a list of cell dictionaries. + """ + return [ + [ + {'value': getattr(obj, attr), 'class': cls, 'type': 'text'} # Adjust 'type' as needed + for attr, cls in column_attrs + ] + for obj in model_objects + ] diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index 1f9dd9c..6398d43 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -64,6 +64,53 @@ {% endmacro %} +{% macro render_selectable_table(headers, rows, selectable, id) %} +
+
+ + + + {% if selectable %} + + {% endif %} + {% for header in headers %} + + {% endfor %} + + + + {% for row in rows %} + + {% if selectable %} + + {% endif %} + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
Select{{ header }}
+ {% if cell.type == 'image' %} +
+
+ +
+
+ {% elif cell.type == 'text' %} +

{{ cell.value }}

+ {% elif cell.type == 'badge' %} + {{ cell.value }} + {% elif cell.type == 'link' %} + {{ cell.value }} + {% else %} + {{ cell.value }} + {% endif %} +
+
+
+{% endmacro %} + {% macro render_accordion(accordion_id, accordion_items, header_title, header_description) %}
diff --git a/eveai_app/templates/navbar.html b/eveai_app/templates/navbar.html index b0118fe..39b5c78 100644 --- a/eveai_app/templates/navbar.html +++ b/eveai_app/templates/navbar.html @@ -69,10 +69,13 @@