6 Commits
v1.1.0 ... main

Author SHA1 Message Date
14cf5e84d3 Remove backward compatibility fields from product forms
Remove automatic addition of ProductName and Price fields when they're not defined as custom AR fields. This ensures tenants using only custom fields won't see unwanted default fields in their products.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 23:14:48 -04:00
f77161da17 Fix image URLs to work behind nginx proxy
Use configured server URL instead of request.url_root for building absolute image URLs in API responses. This ensures Knox Capture receives correct public URLs when the application is behind an nginx proxy.
2025-10-20 23:06:36 -04:00
4d4dbba010 Fix image update issue by adding cache-busting timestamps
Add timestamp query parameters to image URLs to prevent browser caching from showing stale images after updates. Images now display immediately after being updated.
2025-10-20 22:56:07 -04:00
4f2dd12f23 Fix default admin password for new tenants
Changed default password from 'password' to 'admin' to match the default username, allowing authentication to work immediately after tenant creation.
2025-10-20 22:51:55 -04:00
996ecacb40 Add client-side whitespace trimming for AR field names and labels
- Add form submit listener to trim field name and label inputs
- Provides immediate feedback to users before server-side validation
- Creates defense-in-depth approach with JS, route, and model layer trimming
2025-10-20 22:32:31 -04:00
66b88fec03 Strip whitespace from AR field names and labels to prevent image lookup issues
- Add .strip() to field name and label inputs in AR field management
- Implement defensive stripping at model layer for extra safety
- Prevents spacing issues that cause image serving routes to fail
2025-10-20 22:30:54 -04:00
7 changed files with 48 additions and 112 deletions

View File

@@ -3,6 +3,7 @@ from . import admin_bp
from app.models import TenantModel, ProductModel, ARFieldModel, UserModel from app.models import TenantModel, ProductModel, ARFieldModel, UserModel
from app.services import ProductService from app.services import ProductService
from app.decorators.auth import tenant_access_required from app.decorators.auth import tenant_access_required
import time
@admin_bp.route('/add', methods=['GET', 'POST']) @admin_bp.route('/add', methods=['GET', 'POST'])
@tenant_access_required @tenant_access_required
@@ -78,7 +79,9 @@ def add_product(tenant_id):
}) })
field_suffix = field_name[1:] if field_name.startswith('_') else field_name field_suffix = field_name[1:] if field_name.startswith('_') else field_name
image_path = f"/images/{product_id}_{field_suffix}.{extension}" # Add timestamp for cache busting
timestamp = int(time.time())
image_path = f"/images/{product_id}_{field_suffix}.{extension}?v={timestamp}"
if field_name == '_image': if field_name == '_image':
image_data = img_data image_data = img_data
@@ -105,29 +108,6 @@ def add_product(tenant_id):
"fieldType": field['fieldType'] "fieldType": field['fieldType']
}) })
# Handle backward compatibility fields
if '_name' not in [f['fieldName'] for f in custom_fields]:
name = request.form.get('name', '')
if name:
product_data.append({
"fieldName": "_name",
"label": "Product Name",
"value": name,
"editable": "true",
"fieldType": "TEXT"
})
if '_price' not in [f['fieldName'] for f in custom_fields]:
price = request.form.get('price', '')
if price:
product_data.append({
"fieldName": "_price",
"label": "Sale Price",
"value": f"${price}",
"editable": "true",
"fieldType": "TEXT"
})
# Save to database # Save to database
ProductModel.save(product_id, tenant_id, product_data, image_data, image_mime_type) ProductModel.save(product_id, tenant_id, product_data, image_data, image_mime_type)
@@ -217,7 +197,9 @@ def edit_product(tenant_id, product_id):
}) })
field_suffix = field_name[1:] if field_name.startswith('_') else field_name field_suffix = field_name[1:] if field_name.startswith('_') else field_name
existing_field["value"] = f"/images/{product_id}_{field_suffix}.{extension}" # Add timestamp for cache busting
timestamp = int(time.time())
existing_field["value"] = f"/images/{product_id}_{field_suffix}.{extension}?v={timestamp}"
if field_name == '_image': if field_name == '_image':
image_data = img_data image_data = img_data
@@ -230,25 +212,6 @@ def edit_product(tenant_id, product_id):
updated_product.append(existing_field) updated_product.append(existing_field)
# Handle backward compatibility fields
if '_name' not in [f['fieldName'] for f in custom_fields]:
name = request.form.get('name', '')
if name:
for field in products[product_id]:
if field["fieldName"] == "_name":
field["value"] = name
updated_product.append(field)
break
if '_price' not in [f['fieldName'] for f in custom_fields]:
price = request.form.get('price', '')
if price:
for field in products[product_id]:
if field["fieldName"] == "_price":
field["value"] = f"${price}"
updated_product.append(field)
break
# Save to database # Save to database
ProductModel.save(product_id, tenant_id, updated_product, image_data, image_mime_type) ProductModel.save(product_id, tenant_id, updated_product, image_data, image_mime_type)
@@ -321,8 +284,8 @@ def manage_ar_fields(tenant_id):
if action == 'add': if action == 'add':
field_data = { field_data = {
'fieldName': request.form.get('fieldName'), 'fieldName': request.form.get('fieldName', '').strip(),
'label': request.form.get('label'), 'label': request.form.get('label', '').strip(),
'fieldType': request.form.get('fieldType'), 'fieldType': request.form.get('fieldType'),
'editable': request.form.get('editable', 'true'), 'editable': request.form.get('editable', 'true'),
'displayOrder': int(request.form.get('displayOrder', 0)) 'displayOrder': int(request.form.get('displayOrder', 0))
@@ -343,8 +306,8 @@ def manage_ar_fields(tenant_id):
elif action == 'update': elif action == 'update':
field_data = { field_data = {
'id': int(request.form.get('field_id')), 'id': int(request.form.get('field_id')),
'fieldName': request.form.get('fieldName'), 'fieldName': request.form.get('fieldName', '').strip(),
'label': request.form.get('label'), 'label': request.form.get('label', '').strip(),
'fieldType': request.form.get('fieldType'), 'fieldType': request.form.get('fieldType'),
'editable': request.form.get('editable', 'true'), 'editable': request.form.get('editable', 'true'),
'displayOrder': int(request.form.get('displayOrder', 0)) 'displayOrder': int(request.form.get('displayOrder', 0))

View File

@@ -36,6 +36,10 @@ class ARFieldModel:
"""Save or update a custom AR field""" """Save or update a custom AR field"""
tenant_id = tenant_id.lower() tenant_id = tenant_id.lower()
# Strip whitespace from field name and label to prevent spacing issues
field_name = field_data['fieldName'].strip() if field_data.get('fieldName') else ''
label = field_data['label'].strip() if field_data.get('label') else ''
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@@ -47,8 +51,8 @@ class ARFieldModel:
display_order = ?, updated_at = CURRENT_TIMESTAMP display_order = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND tenant_id = ? WHERE id = ? AND tenant_id = ?
''', ( ''', (
field_data['fieldName'], field_name,
field_data['label'], label,
field_data['fieldType'], field_data['fieldType'],
field_data['editable'], field_data['editable'],
field_data['displayOrder'], field_data['displayOrder'],
@@ -63,8 +67,8 @@ class ARFieldModel:
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
''', ( ''', (
tenant_id, tenant_id,
field_data['fieldName'], field_name,
field_data['label'], label,
field_data['fieldType'], field_data['fieldType'],
field_data['editable'], field_data['editable'],
field_data['displayOrder'] field_data['displayOrder']

View File

@@ -37,7 +37,7 @@ class TenantModel:
try: try:
cursor.execute( cursor.execute(
'INSERT INTO tenants (id, name, username, password) VALUES (?, ?, ?, ?)', 'INSERT INTO tenants (id, name, username, password) VALUES (?, ?, ?, ?)',
(tenant_id, name, 'admin', 'password') (tenant_id, name, 'admin', 'admin')
) )
conn.commit() conn.commit()
return TenantModel.get_by_id(tenant_id) return TenantModel.get_by_id(tenant_id)

View File

@@ -1,5 +1,5 @@
from typing import Dict, List, Any from typing import Dict, List, Any
from app.models import ProductModel, ARFieldModel from app.models import ProductModel, ARFieldModel, SettingsModel
from flask import request from flask import request
class ProductService: class ProductService:
@@ -20,8 +20,9 @@ class ProductService:
if field_types.get(field['fieldName']) == 'IMAGE_URI' and field['value']: if field_types.get(field['fieldName']) == 'IMAGE_URI' and field['value']:
# If it's already an absolute URL, leave it as is # If it's already an absolute URL, leave it as is
if not field['value'].startswith('http'): if not field['value'].startswith('http'):
# Build absolute URL using request host with tenant # Build absolute URL using configured server URL
field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}" server_url = SettingsModel.get_server_url()
field['value'] = f"{server_url}/{tenant_id}{field['value']}"
filtered_fields.append(field) filtered_fields.append(field)
return filtered_fields return filtered_fields

View File

@@ -62,25 +62,7 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<!-- Fallback fields for backward compatibility -->
{% if not custom_fields|selectattr('fieldName', 'equalto', '_name')|list %}
<div class="mb-3">
<label for="name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="name" name="name">
</div>
{% endif %}
{% if not custom_fields|selectattr('fieldName', 'equalto', '_price')|list %}
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="price" name="price" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="49.99">
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="/{{ tenant_id }}/" class="btn btn-secondary">Cancel</a> <a href="/{{ tenant_id }}/" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Add Product</button> <button type="submit" class="btn btn-primary">Add Product</button>

View File

@@ -111,6 +111,20 @@
</div> </div>
<script> <script>
// Trim whitespace from field name and label on form submission
document.getElementById('fieldForm').addEventListener('submit', function(e) {
var fieldNameInput = document.getElementById('fieldName');
var labelInput = document.getElementById('label');
// Trim whitespace from inputs before submission
if (fieldNameInput && !fieldNameInput.disabled) {
fieldNameInput.value = fieldNameInput.value.trim();
}
if (labelInput) {
labelInput.value = labelInput.value.trim();
}
});
function editField(button) { function editField(button) {
// Get data from button attributes // Get data from button attributes
var fieldData = { var fieldData = {
@@ -121,7 +135,7 @@ function editField(button) {
editable: button.getAttribute('data-editable'), editable: button.getAttribute('data-editable'),
displayOrder: button.getAttribute('data-displayorder') displayOrder: button.getAttribute('data-displayorder')
}; };
// Populate form // Populate form
document.getElementById('formAction').value = 'update'; document.getElementById('formAction').value = 'update';
document.getElementById('fieldId').value = fieldData.id; document.getElementById('fieldId').value = fieldData.id;
@@ -130,20 +144,20 @@ function editField(button) {
document.getElementById('fieldType').value = fieldData.fieldType; document.getElementById('fieldType').value = fieldData.fieldType;
document.getElementById('editable').value = fieldData.editable; document.getElementById('editable').value = fieldData.editable;
document.getElementById('displayOrder').value = fieldData.displayOrder; document.getElementById('displayOrder').value = fieldData.displayOrder;
// Disable field name when editing and show warning // Disable field name when editing and show warning
document.getElementById('fieldName').disabled = true; document.getElementById('fieldName').disabled = true;
document.getElementById('fieldNameWarning').style.display = 'block'; document.getElementById('fieldNameWarning').style.display = 'block';
// Update form heading // Update form heading
var formCard = document.querySelector('.card:last-child .card-header h5'); var formCard = document.querySelector('.card:last-child .card-header h5');
if (formCard) { if (formCard) {
formCard.textContent = 'Edit AR Field'; formCard.textContent = 'Edit AR Field';
} }
// Change button text // Change button text
document.querySelector('button[type="submit"]').textContent = 'Update Field'; document.querySelector('button[type="submit"]').textContent = 'Update Field';
// Scroll to form // Scroll to form
document.getElementById('fieldForm').scrollIntoView({ behavior: 'smooth' }); document.getElementById('fieldForm').scrollIntoView({ behavior: 'smooth' });
} }
@@ -152,17 +166,17 @@ function resetForm() {
document.getElementById('formAction').value = 'add'; document.getElementById('formAction').value = 'add';
document.getElementById('fieldId').value = ''; document.getElementById('fieldId').value = '';
document.getElementById('fieldForm').reset(); document.getElementById('fieldForm').reset();
// Enable field name and hide warning // Enable field name and hide warning
document.getElementById('fieldName').disabled = false; document.getElementById('fieldName').disabled = false;
document.getElementById('fieldNameWarning').style.display = 'none'; document.getElementById('fieldNameWarning').style.display = 'none';
// Reset form heading // Reset form heading
var formCard = document.querySelector('.card:last-child .card-header h5'); var formCard = document.querySelector('.card:last-child .card-header h5');
if (formCard) { if (formCard) {
formCard.textContent = 'Add/Edit AR Field'; formCard.textContent = 'Add/Edit AR Field';
} }
// Reset button text // Reset button text
document.querySelector('button[type="submit"]').textContent = 'Save Field'; document.querySelector('button[type="submit"]').textContent = 'Save Field';
} }

View File

@@ -60,35 +60,7 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<!-- Fallback fields for backward compatibility -->
{% if not custom_fields|selectattr('fieldName', 'equalto', '_name')|list %}
{% for field in product %}
{% if field.fieldName == '_name' %}
<div class="mb-3">
<label for="name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ field.value }}">
</div>
{% endif %}
{% endfor %}
{% endif %}
{% if not custom_fields|selectattr('fieldName', 'equalto', '_price')|list %}
{% for field in product %}
{% if field.fieldName == '_price' %}
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
{% set price_value = field.value|replace('$', '') %}
<input type="text" class="form-control" id="price" name="price"
pattern="[0-9]+(\.[0-9]{1,2})?" value="{{ price_value }}" placeholder="49.99">
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="/{{ tenant_id }}/" class="btn btn-secondary">Cancel</a> <a href="/{{ tenant_id }}/" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Update Product</button> <button type="submit" class="btn btn-primary">Update Product</button>