225 lines
9.0 KiB
Python
225 lines
9.0 KiB
Python
|
|
from flask import render_template, request, redirect, flash, jsonify, Response, current_app
|
||
|
|
from . import tenant_bp
|
||
|
|
from app.models import TenantModel, ProductModel, ARFieldModel, SettingsModel
|
||
|
|
from app.services import AuthService, ProductService, BarcodeService
|
||
|
|
import os
|
||
|
|
|
||
|
|
@tenant_bp.route('/')
|
||
|
|
def index(tenant_id):
|
||
|
|
"""Tenant home page - product listing"""
|
||
|
|
tenant = TenantModel.get_or_create(tenant_id)
|
||
|
|
if tenant is None:
|
||
|
|
return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404
|
||
|
|
|
||
|
|
products = ProductModel.get_all(tenant_id)
|
||
|
|
custom_fields = ARFieldModel.get_all(tenant_id)
|
||
|
|
|
||
|
|
return render_template('index.html', tenant=tenant, products=products, custom_fields=custom_fields)
|
||
|
|
|
||
|
|
@tenant_bp.route('/delete', methods=['POST'])
|
||
|
|
def delete_tenant(tenant_id):
|
||
|
|
"""Delete a tenant and all its data"""
|
||
|
|
TenantModel.delete(tenant_id)
|
||
|
|
flash(f'Tenant "{tenant_id}" has been deleted successfully.', 'success')
|
||
|
|
return redirect('/')
|
||
|
|
|
||
|
|
@tenant_bp.route('/settings')
|
||
|
|
def settings(tenant_id):
|
||
|
|
"""Tenant settings page"""
|
||
|
|
tenant = TenantModel.get_or_create(tenant_id)
|
||
|
|
if tenant is None:
|
||
|
|
return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404
|
||
|
|
|
||
|
|
custom_fields = ARFieldModel.get_all(tenant_id)
|
||
|
|
server_url = SettingsModel.get_server_url()
|
||
|
|
|
||
|
|
return render_template('tenant_settings.html', tenant=tenant, custom_fields=custom_fields, server_url=server_url)
|
||
|
|
|
||
|
|
@tenant_bp.route('/settings/credentials', methods=['POST'])
|
||
|
|
def update_credentials(tenant_id):
|
||
|
|
"""Update tenant credentials"""
|
||
|
|
tenant = TenantModel.get_by_id(tenant_id)
|
||
|
|
if tenant is None:
|
||
|
|
return jsonify({"error": "Tenant not found"}), 404
|
||
|
|
|
||
|
|
username = request.form.get('username', '').strip()
|
||
|
|
password = request.form.get('password', '').strip()
|
||
|
|
|
||
|
|
if not username:
|
||
|
|
flash('Username is required.', 'error')
|
||
|
|
return redirect(f'/{tenant_id}/settings')
|
||
|
|
|
||
|
|
TenantModel.update_credentials(tenant_id, username, password if password else None)
|
||
|
|
flash('Credentials updated successfully.', 'success')
|
||
|
|
return redirect(f'/{tenant_id}/settings')
|
||
|
|
|
||
|
|
@tenant_bp.route('/settings/barcode', methods=['POST'])
|
||
|
|
def update_barcode_type(tenant_id):
|
||
|
|
"""Update tenant barcode type"""
|
||
|
|
tenant = TenantModel.get_by_id(tenant_id)
|
||
|
|
if tenant is None:
|
||
|
|
return jsonify({"error": "Tenant not found"}), 404
|
||
|
|
|
||
|
|
barcode_type = request.form.get('barcode_type', '').strip()
|
||
|
|
|
||
|
|
if not barcode_type:
|
||
|
|
flash('Barcode type is required.', 'error')
|
||
|
|
return redirect(f'/{tenant_id}/settings')
|
||
|
|
|
||
|
|
TenantModel.update_barcode_type(tenant_id, barcode_type)
|
||
|
|
flash('Barcode type updated successfully.', 'success')
|
||
|
|
return redirect(f'/{tenant_id}/settings')
|
||
|
|
|
||
|
|
@tenant_bp.route('/login', methods=['GET'])
|
||
|
|
def login(tenant_id):
|
||
|
|
"""Login endpoint for API authentication"""
|
||
|
|
auth_header = request.headers.get('Authorization')
|
||
|
|
|
||
|
|
if AuthService.check_basic_auth(auth_header, tenant_id):
|
||
|
|
return jsonify({"status": "success", "message": "Authentication successful"}), 200
|
||
|
|
|
||
|
|
return jsonify({"error": "Unauthorized"}), 401
|
||
|
|
|
||
|
|
@tenant_bp.route('/arcontentfields', methods=['GET'])
|
||
|
|
def get_ar_content_fields(tenant_id):
|
||
|
|
"""Get custom AR fields for tenant"""
|
||
|
|
fields = ARFieldModel.get_all(tenant_id)
|
||
|
|
return jsonify(fields), 200
|
||
|
|
|
||
|
|
@tenant_bp.route('/arinfo', methods=['GET', 'POST'])
|
||
|
|
def get_ar_info(tenant_id):
|
||
|
|
"""Get or update AR product information"""
|
||
|
|
barcode = request.args.get('barcode')
|
||
|
|
|
||
|
|
# Handle POST request - update product fields
|
||
|
|
if request.method == 'POST':
|
||
|
|
if not barcode:
|
||
|
|
return jsonify({"error": "Barcode parameter required"}), 400
|
||
|
|
|
||
|
|
product = ProductModel.get_by_id(barcode, tenant_id)
|
||
|
|
if not product:
|
||
|
|
return jsonify({"error": "Product not found"}), 404
|
||
|
|
|
||
|
|
try:
|
||
|
|
updated_fields = request.get_json()
|
||
|
|
if not isinstance(updated_fields, list):
|
||
|
|
return jsonify({"error": "Request body must be an array of fields"}), 400
|
||
|
|
|
||
|
|
# Update only the editable fields
|
||
|
|
for updated_field in updated_fields:
|
||
|
|
field_name = updated_field.get('fieldName')
|
||
|
|
new_value = updated_field.get('value')
|
||
|
|
|
||
|
|
for field in product:
|
||
|
|
if field['fieldName'] == field_name:
|
||
|
|
if field.get('editable') == 'true':
|
||
|
|
field['value'] = new_value
|
||
|
|
break
|
||
|
|
|
||
|
|
# Save the updated product
|
||
|
|
ProductModel.save(barcode, tenant_id, product)
|
||
|
|
current_app.logger.info(f"Updated product {barcode} for tenant {tenant_id}")
|
||
|
|
return jsonify({"success": True}), 200
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
current_app.logger.error(f"Error updating product: {str(e)}")
|
||
|
|
return jsonify({"error": "Failed to update product"}), 500
|
||
|
|
|
||
|
|
# Handle GET request - return product data
|
||
|
|
if barcode:
|
||
|
|
product_data = ProductService.get_product_filtered(barcode, tenant_id)
|
||
|
|
if product_data:
|
||
|
|
response = jsonify(product_data)
|
||
|
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||
|
|
return response, 200
|
||
|
|
return jsonify({"error": "Product not found"}), 404
|
||
|
|
|
||
|
|
# Return all products if no barcode specified
|
||
|
|
all_products = ProductService.get_all_products_filtered(tenant_id)
|
||
|
|
response = jsonify(all_products)
|
||
|
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||
|
|
return response, 200
|
||
|
|
|
||
|
|
@tenant_bp.route('/images/<path:filename>', methods=['GET'])
|
||
|
|
def serve_image(tenant_id, filename):
|
||
|
|
"""Serve product images"""
|
||
|
|
current_app.logger.info(f"Image requested for tenant {tenant_id}: {filename}")
|
||
|
|
|
||
|
|
# Check if filename contains field name (e.g., "123456_thumbnail.jpg")
|
||
|
|
base_name = os.path.splitext(filename)[0]
|
||
|
|
|
||
|
|
if '_' in base_name:
|
||
|
|
parts = base_name.split('_', 1)
|
||
|
|
product_id = parts[0]
|
||
|
|
field_name = '_' + parts[1] if len(parts) > 1 else '_image'
|
||
|
|
|
||
|
|
# Try to get field-specific image
|
||
|
|
image_data = ProductModel.get_image_by_field(product_id, tenant_id, field_name)
|
||
|
|
if image_data:
|
||
|
|
image_bytes, mime_type = image_data
|
||
|
|
response = Response(image_bytes, mimetype=mime_type)
|
||
|
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||
|
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||
|
|
current_app.logger.info(f"Serving field-specific image: {product_id}/{field_name} ({len(image_bytes)} bytes)")
|
||
|
|
return response
|
||
|
|
|
||
|
|
# Try standard image lookup (backward compatibility)
|
||
|
|
product_id = os.path.splitext(filename)[0]
|
||
|
|
image_data = ProductModel.get_image(product_id, tenant_id)
|
||
|
|
if image_data:
|
||
|
|
image_bytes, mime_type = image_data
|
||
|
|
response = Response(image_bytes, mimetype=mime_type)
|
||
|
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||
|
|
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||
|
|
current_app.logger.info(f"Serving image from database: {product_id} ({len(image_bytes)} bytes)")
|
||
|
|
return response
|
||
|
|
|
||
|
|
current_app.logger.warning(f"Image not found: {filename}")
|
||
|
|
return jsonify({"error": "Image not found"}), 404
|
||
|
|
|
||
|
|
@tenant_bp.route('/qrcode/template', methods=['GET'])
|
||
|
|
def generate_template_qr_code(tenant_id):
|
||
|
|
"""Generate QR code for the AR Template URL"""
|
||
|
|
try:
|
||
|
|
server_url = SettingsModel.get_server_url()
|
||
|
|
template_url = f"{server_url}/{tenant_id}/"
|
||
|
|
buffer = BarcodeService.generate_qr_code(template_url)
|
||
|
|
return Response(buffer.getvalue(), mimetype='image/png')
|
||
|
|
except Exception as e:
|
||
|
|
current_app.logger.error(f"QR code generation error: {str(e)}")
|
||
|
|
return jsonify({"error": "Failed to generate QR code"}), 500
|
||
|
|
|
||
|
|
@tenant_bp.route('/qrcode/arinfo', methods=['GET'])
|
||
|
|
def generate_ar_qr_code(tenant_id):
|
||
|
|
"""Generate QR code for the AR API endpoint"""
|
||
|
|
try:
|
||
|
|
server_url = SettingsModel.get_server_url()
|
||
|
|
ar_url = f"{server_url}/{tenant_id}/arinfo"
|
||
|
|
buffer = BarcodeService.generate_qr_code(ar_url)
|
||
|
|
return Response(buffer.getvalue(), mimetype='image/png')
|
||
|
|
except Exception as e:
|
||
|
|
current_app.logger.error(f"QR code generation error: {str(e)}")
|
||
|
|
return jsonify({"error": "Failed to generate QR code"}), 500
|
||
|
|
|
||
|
|
@tenant_bp.route('/barcodes/<path:filename>', methods=['GET'])
|
||
|
|
def serve_barcode(tenant_id, filename):
|
||
|
|
"""Serve dynamically generated barcodes"""
|
||
|
|
name_parts = os.path.splitext(filename)[0].split('_')
|
||
|
|
if len(name_parts) < 2:
|
||
|
|
return jsonify({"error": "Invalid barcode filename format"}), 400
|
||
|
|
|
||
|
|
product_id = '_'.join(name_parts[:-1])
|
||
|
|
code_type = name_parts[-1].lower()
|
||
|
|
|
||
|
|
try:
|
||
|
|
url = None
|
||
|
|
if code_type == 'qr':
|
||
|
|
url = f'http://{request.host}/{tenant_id}/arinfo?barcode={product_id}'
|
||
|
|
|
||
|
|
buffer = BarcodeService.generate_barcode(product_id, code_type, url)
|
||
|
|
return Response(buffer.getvalue(), mimetype='image/png')
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
current_app.logger.error(f"Barcode generation error: {str(e)}")
|
||
|
|
return jsonify({"error": "Failed to generate barcode"}), 500
|