From 9f33e1aeb68dc4556eda1575158f65b566209f6c Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Thu, 19 Jun 2025 16:08:18 -0400 Subject: [PATCH] case insensitivity --- src/app.py | 9 +++++ src/database.py | 59 ++++++++++++++++++++++++++++- src/templates/tenant_selection.html | 7 +++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/app.py b/src/app.py index f94377d..af741a4 100644 --- a/src/app.py +++ b/src/app.py @@ -8,6 +8,7 @@ from barcode.writer import ImageWriter from io import BytesIO import shutil from werkzeug.routing import BaseConverter +from functools import wraps app = Flask(__name__) # Set DATA_FOLDER to the absolute path of the data directory inside src @@ -17,6 +18,14 @@ app.config['SECRET_KEY'] = 'dev-key-for-demo-only' # Custom converter for tenant IDs class TenantConverter(BaseConverter): regex = '[a-zA-Z0-9_-]+' + + def to_python(self, value): + # Convert to lowercase when parsing from URL + return value.lower() + + def to_url(self, value): + # Convert to lowercase when generating URLs + return value.lower() app.url_map.converters['tenant'] = TenantConverter diff --git a/src/database.py b/src/database.py index 836f9a6..01c71d4 100644 --- a/src/database.py +++ b/src/database.py @@ -204,6 +204,10 @@ def migrate_from_json(): def get_all_products(tenant_id: str = None) -> Dict[str, List[Dict[str, Any]]]: """Get all products for a tenant in the legacy format""" + # Normalize tenant_id to lowercase if provided + if tenant_id: + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -248,6 +252,9 @@ def get_all_products(tenant_id: str = None) -> Dict[str, List[Dict[str, Any]]]: def get_product(product_id: str, tenant_id: str) -> Optional[List[Dict[str, Any]]]: """Get a single product by ID and tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -272,6 +279,9 @@ def get_product(product_id: str, tenant_id: str) -> Optional[List[Dict[str, Any] def save_product(product_id: str, tenant_id: str, fields: List[Dict[str, Any]], image_data: Optional[bytes] = None, image_mime_type: Optional[str] = None): """Save or update a product for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -336,6 +346,9 @@ def save_product(product_id: str, tenant_id: str, fields: List[Dict[str, Any]], def delete_product(product_id: str, tenant_id: str): """Delete a product for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute('DELETE FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) @@ -343,6 +356,9 @@ def delete_product(product_id: str, tenant_id: str): def get_product_image(product_id: str, tenant_id: str) -> Optional[tuple[bytes, str]]: """Get product image data and mime type for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute('SELECT image_data, image_mime_type FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) @@ -420,6 +436,9 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N def update_tenant_credentials(tenant_id: str, username: str, password: str): """Update tenant credentials""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -448,6 +467,9 @@ def get_all_tenants() -> List[Dict[str, Any]]: def delete_tenant(tenant_id: str): """Delete a tenant and all associated data""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() # Due to ON DELETE CASCADE, this will also delete all products and product_fields @@ -475,6 +497,9 @@ def cleanup_reserved_tenants(): def get_custom_ar_fields(tenant_id: str) -> List[Dict[str, Any]]: """Get custom AR fields for a tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -499,6 +524,9 @@ def get_custom_ar_fields(tenant_id: str) -> List[Dict[str, Any]]: def save_custom_ar_field(tenant_id: str, field_data: Dict[str, Any]) -> int: """Save or update a custom AR field""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -539,6 +567,9 @@ def save_custom_ar_field(tenant_id: str, field_data: Dict[str, Any]) -> int: def delete_custom_ar_field(tenant_id: str, field_id: int): """Delete a custom AR field""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -549,17 +580,38 @@ def delete_custom_ar_field(tenant_id: str, field_id: int): def init_default_ar_fields(tenant_id: str): """Initialize default AR fields for a new tenant""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + default_fields = [ {"fieldName": "_id", "label": "Item ID", "fieldType": "TEXT", "editable": "false", "displayOrder": 1}, {"fieldName": "_price", "label": "Sale Price", "fieldType": "TEXT", "editable": "true", "displayOrder": 2}, {"fieldName": "_image", "label": "Image", "fieldType": "IMAGE_URI", "editable": "false", "displayOrder": 3} ] - for field in default_fields: - save_custom_ar_field(tenant_id, field) + with get_db() as conn: + cursor = conn.cursor() + for field in default_fields: + # Use INSERT OR IGNORE to avoid duplicate key errors + cursor.execute(''' + INSERT OR IGNORE INTO custom_ar_fields + (tenant_id, field_name, label, field_type, editable, display_order) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + tenant_id, + field['fieldName'], + field['label'], + field['fieldType'], + field.get('editable', 'true'), + field.get('displayOrder', 0) + )) + conn.commit() def save_product_image(product_id: str, tenant_id: str, field_name: str, image_data: bytes, mime_type: str): """Save an image for a specific field of a product""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() @@ -579,6 +631,9 @@ def save_product_image(product_id: str, tenant_id: str, field_name: str, image_d def get_product_image_by_field(product_id: str, tenant_id: str, field_name: str) -> Optional[tuple[bytes, str]]: """Get image data for a specific field of a product""" + # Normalize tenant_id to lowercase + tenant_id = tenant_id.lower() + with get_db() as conn: cursor = conn.cursor() cursor.execute(''' diff --git a/src/templates/tenant_selection.html b/src/templates/tenant_selection.html index 4dd29ed..5e2012f 100644 --- a/src/templates/tenant_selection.html +++ b/src/templates/tenant_selection.html @@ -90,11 +90,14 @@ e.preventDefault(); const tenantId = document.getElementById('tenantId').value; if (tenantId) { - if (reservedIds.includes(tenantId.toLowerCase())) { + // Convert to lowercase for consistency + const normalizedId = tenantId.toLowerCase(); + if (reservedIds.includes(normalizedId)) { alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`); return; } - window.location.href = '/' + tenantId + '/'; + // Use the normalized (lowercase) ID in the URL + window.location.href = '/' + normalizedId + '/'; } });