diff --git a/src/app.py b/src/app.py index d46944d..a154996 100644 --- a/src/app.py +++ b/src/app.py @@ -1,34 +1,43 @@ -from flask import Flask, jsonify, request, send_file, render_template, flash, Response +from flask import Flask, jsonify, request, send_file, render_template, flash, Response, redirect import os import base64 -from routes.admin import admin_bp -from routes.api import api_bp import database import qrcode import barcode from barcode.writer import ImageWriter from io import BytesIO import shutil +from werkzeug.routing import BaseConverter app = Flask(__name__) # Set DATA_FOLDER to the absolute path of the data directory inside src app.config['DATA_FOLDER'] = os.path.join(os.path.dirname(__file__), 'data') app.config['SECRET_KEY'] = 'dev-key-for-demo-only' -# Register blueprints -app.register_blueprint(admin_bp) -app.register_blueprint(api_bp) +# Custom converter for tenant IDs +class TenantConverter(BaseConverter): + regex = '[a-zA-Z0-9_-]+' + +app.url_map.converters['tenant'] = TenantConverter # Load product data from database -def load_products(): - return database.get_all_products() +def load_products(tenant_id=None): + return database.get_all_products(tenant_id) @app.route('/') def index(): - return render_template('index.html') + # Show a tenant selection page or redirect to default + tenants = database.get_all_tenants() + return render_template('tenant_selection.html', tenants=tenants) -def check_basic_auth(auth_header): - """Validate Basic authentication credentials""" +@app.route('//') +def tenant_index(tenant_id): + # Auto-create tenant if it doesn't exist + tenant = database.get_or_create_tenant(tenant_id) + return render_template('index.html', tenant=tenant) + +def check_basic_auth(auth_header, tenant_id): + """Validate Basic authentication credentials for a tenant""" if not auth_header or not auth_header.startswith('Basic '): return False @@ -37,27 +46,27 @@ def check_basic_auth(auth_header): credentials = base64.b64decode(auth_header[6:]).decode('utf-8') username, password = credentials.split(':', 1) - # Simple hardcoded credentials - in production, use secure storage - # You can change these credentials as needed - if username == 'admin' and password == 'knox123': + # Get tenant credentials from database + tenant = database.get_or_create_tenant(tenant_id) + if username == tenant['username'] and password == tenant['password']: return True except Exception: pass return False -@app.route('/login', methods=['GET']) -def login(): +@app.route('//login', methods=['GET']) +def login(tenant_id): auth_header = request.headers.get('Authorization') - if check_basic_auth(auth_header): + if check_basic_auth(auth_header, tenant_id): return jsonify({"status": "success", "message": "Authentication successful"}), 200 return jsonify({"error": "Unauthorized"}), 401 -@app.route('/arcontentfields', methods=['GET']) -def get_ar_content_fields(): - # Return a fixed set of attributes +@app.route('//arcontentfields', methods=['GET']) +def get_ar_content_fields(tenant_id): + # Return a fixed set of attributes (tenant_id could be used for custom fields in the future) fields = [ {"fieldName": "_id", "label": "Item ID", "editable": "false", "fieldType": "TEXT"}, {"fieldName": "_price", "label": "Sale Price", "editable": "true", "fieldType": "TEXT"}, @@ -65,10 +74,10 @@ def get_ar_content_fields(): ] return jsonify(fields), 200 -@app.route('/arinfo', methods=['GET']) -def get_ar_info(): +@app.route('//arinfo', methods=['GET']) +def get_ar_info(tenant_id): barcode = request.args.get('barcode') - products = load_products() + products = load_products(tenant_id) # Helper function to convert relative image paths to absolute URLs def make_absolute_urls(product_fields): @@ -77,8 +86,8 @@ def get_ar_info(): if field['fieldName'] == '_image' and field['value']: # If it's already an absolute URL, leave it as is if not field['value'].startswith('http'): - # Build absolute URL using request host - field['value'] = f"{request.url_root.rstrip('/')}{field['value']}" + # Build absolute URL using request host with tenant + field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}" return product_fields # If barcode is provided, return specific product @@ -99,16 +108,16 @@ def get_ar_info(): response.headers['Access-Control-Allow-Origin'] = '*' return response, 200 -@app.route('/images/', methods=['GET']) -def serve_image(filename): +@app.route('//images/', methods=['GET']) +def serve_image(tenant_id, filename): # Log the request for debugging - app.logger.info(f"Image requested: {filename}") + app.logger.info(f"Image requested for tenant {tenant_id}: {filename}") # Extract product ID from filename (e.g., "123456.jpg" -> "123456") product_id = os.path.splitext(filename)[0] - # Get image from database - image_data = database.get_product_image(product_id) + # Get image from database for this tenant + image_data = database.get_product_image(product_id, tenant_id) if image_data: image_bytes, mime_type = image_data response = Response(image_bytes, mimetype=mime_type) @@ -129,8 +138,8 @@ def serve_image(filename): app.logger.warning(f"Image not found: {filename}") return jsonify({"error": "Image not found"}), 404 -@app.route('/barcodes/', methods=['GET']) -def serve_barcode(filename): +@app.route('//barcodes/', methods=['GET']) +def serve_barcode(tenant_id, filename): # Parse filename to extract product_id and barcode type # Expected format: {product_id}_{type}.png name_parts = os.path.splitext(filename)[0].split('_') @@ -145,9 +154,9 @@ def serve_barcode(filename): buffer = BytesIO() if code_type == 'qr': - # Generate QR code + # Generate QR code with tenant in URL qr = qrcode.QRCode(version=1, box_size=10, border=5) - qr.add_data(f'http://{request.host}/arinfo?barcode={product_id}') + qr.add_data(f'http://{request.host}/{tenant_id}/arinfo?barcode={product_id}') qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") img.save(buffer, format='PNG') @@ -181,6 +190,31 @@ def serve_barcode(filename): app.logger.error(f"Barcode generation error: {str(e)}") return jsonify({"error": "Failed to generate barcode"}), 500 +# Import the admin routes directly and register them with tenant support +from routes.admin import (index as admin_index, add_product, edit_product, + delete_product, view_product, generate_barcode, + generate_barcode_page, manage_credentials) + +# Register admin routes with tenant prefix +app.add_url_rule('//admin/', 'admin.index', admin_index) +app.add_url_rule('//admin/add', 'admin.add_product', add_product, methods=['GET', 'POST']) +app.add_url_rule('//admin/edit/', 'admin.edit_product', edit_product, methods=['GET', 'POST']) +app.add_url_rule('//admin/delete/', 'admin.delete_product', delete_product, methods=['POST']) +app.add_url_rule('//admin/view/', 'admin.view_product', view_product) +app.add_url_rule('//admin/generate_barcode//', 'admin.generate_barcode', generate_barcode) +app.add_url_rule('//admin/generate_barcode_page/', 'admin.generate_barcode_page', generate_barcode_page) +app.add_url_rule('//admin/credentials', 'admin.manage_credentials', manage_credentials, methods=['GET', 'POST']) + +# Register API routes with tenant prefix +from routes.api import api_index +app.add_url_rule('//api/', 'api.api_index', api_index) + +# Catch-all route for admin without tenant - redirect to home +@app.route('/admin/') +@app.route('/admin/') +def redirect_admin_to_home(path=None): + return redirect('/') + if __name__ == '__main__': # Initialize database database.init_database() diff --git a/src/database.py b/src/database.py index cf4d7c4..a759c82 100644 --- a/src/database.py +++ b/src/database.py @@ -24,38 +24,54 @@ def init_database(): with get_db() as conn: cursor = conn.cursor() - # Create products table with image_data as BLOB + # Create tenants table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS tenants ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + username TEXT, + password TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create products table with tenant_id cursor.execute(''' CREATE TABLE IF NOT EXISTS products ( - id TEXT PRIMARY KEY, + id TEXT, + tenant_id TEXT NOT NULL, name TEXT NOT NULL, price TEXT, inventory INTEGER, image_data BLOB, image_mime_type TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, tenant_id), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) ''') - # Create product_fields table for extensible fields + # Create product_fields table with tenant_id cursor.execute(''' CREATE TABLE IF NOT EXISTS product_fields ( product_id TEXT, + tenant_id TEXT, field_name TEXT, label TEXT, value TEXT, editable TEXT, field_type TEXT, - FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, - PRIMARY KEY (product_id, field_name) + FOREIGN KEY (product_id, tenant_id) REFERENCES products(id, tenant_id) ON DELETE CASCADE, + PRIMARY KEY (product_id, tenant_id, field_name) ) ''') conn.commit() def migrate_from_json(): - """Migrate existing JSON data to SQLite""" + """Migrate existing JSON data to SQLite with default tenant""" json_path = os.path.join(os.path.dirname(__file__), 'data', 'products.json') if not os.path.exists(json_path): @@ -67,6 +83,13 @@ def migrate_from_json(): with get_db() as conn: cursor = conn.cursor() + # Create default tenant if not exists + default_tenant_id = 'default' + cursor.execute(''' + INSERT OR IGNORE INTO tenants (id, name, username, password) + VALUES (?, ?, ?, ?) + ''', (default_tenant_id, 'Default', 'admin', 'admin')) + for product_id, fields in products_data.items(): # Extract core fields name = '' @@ -106,20 +129,21 @@ def migrate_from_json(): } image_mime_type = mime_types.get(ext, 'image/jpeg') - # Insert into products table + # Insert into products table with tenant_id cursor.execute(''' - INSERT OR REPLACE INTO products (id, name, price, inventory, image_data, image_mime_type) - VALUES (?, ?, ?, ?, ?, ?) - ''', (product_id, name, price, inventory, image_data, image_mime_type)) + INSERT OR REPLACE INTO products (id, tenant_id, name, price, inventory, image_data, image_mime_type) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (product_id, default_tenant_id, name, price, inventory, image_data, image_mime_type)) - # Insert all fields into product_fields table + # Insert all fields into product_fields table with tenant_id for field in fields: cursor.execute(''' INSERT OR REPLACE INTO product_fields - (product_id, field_name, label, value, editable, field_type) - VALUES (?, ?, ?, ?, ?, ?) + (product_id, tenant_id, field_name, label, value, editable, field_type) + VALUES (?, ?, ?, ?, ?, ?, ?) ''', ( product_id, + default_tenant_id, field['fieldName'], field['label'], field['value'], @@ -129,24 +153,35 @@ def migrate_from_json(): conn.commit() -def get_all_products() -> Dict[str, List[Dict[str, Any]]]: - """Get all products in the legacy format""" +def get_all_products(tenant_id: str = None) -> Dict[str, List[Dict[str, Any]]]: + """Get all products for a tenant in the legacy format""" with get_db() as conn: cursor = conn.cursor() - # Get all product IDs - cursor.execute('SELECT id FROM products') + # Get all product IDs for the tenant + if tenant_id: + cursor.execute('SELECT id FROM products WHERE tenant_id = ?', (tenant_id,)) + else: + cursor.execute('SELECT id FROM products') product_ids = [row['id'] for row in cursor.fetchall()] result = {} for product_id in product_ids: # Get all fields for this product - cursor.execute(''' - SELECT field_name, label, value, editable, field_type - FROM product_fields - WHERE product_id = ? - ORDER BY field_name - ''', (product_id,)) + if tenant_id: + cursor.execute(''' + SELECT field_name, label, value, editable, field_type + FROM product_fields + WHERE product_id = ? AND tenant_id = ? + ORDER BY field_name + ''', (product_id, tenant_id)) + else: + cursor.execute(''' + SELECT field_name, label, value, editable, field_type + FROM product_fields + WHERE product_id = ? + ORDER BY field_name + ''', (product_id,)) fields = [] for row in cursor.fetchall(): @@ -162,17 +197,17 @@ def get_all_products() -> Dict[str, List[Dict[str, Any]]]: return result -def get_product(product_id: str) -> Optional[List[Dict[str, Any]]]: - """Get a single product by ID""" +def get_product(product_id: str, tenant_id: str) -> Optional[List[Dict[str, Any]]]: + """Get a single product by ID and tenant""" with get_db() as conn: cursor = conn.cursor() cursor.execute(''' SELECT field_name, label, value, editable, field_type FROM product_fields - WHERE product_id = ? + WHERE product_id = ? AND tenant_id = ? ORDER BY field_name - ''', (product_id,)) + ''', (product_id, tenant_id)) fields = [] for row in cursor.fetchall(): @@ -186,8 +221,8 @@ def get_product(product_id: str) -> Optional[List[Dict[str, Any]]]: return fields if fields else None -def save_product(product_id: str, fields: List[Dict[str, Any]], image_data: Optional[bytes] = None, image_mime_type: Optional[str] = None): - """Save or update a product""" +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""" with get_db() as conn: cursor = conn.cursor() @@ -205,7 +240,7 @@ def save_product(product_id: str, fields: List[Dict[str, Any]], image_data: Opti inventory = int(field['value']) if field['value'] else None # Check if product exists - cursor.execute('SELECT id FROM products WHERE id = ?', (product_id,)) + cursor.execute('SELECT id FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) exists = cursor.fetchone() is not None if exists: @@ -214,32 +249,33 @@ def save_product(product_id: str, fields: List[Dict[str, Any]], image_data: Opti cursor.execute(''' UPDATE products SET name = ?, price = ?, inventory = ?, image_data = ?, image_mime_type = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - ''', (name, price, inventory, image_data, image_mime_type, product_id)) + WHERE id = ? AND tenant_id = ? + ''', (name, price, inventory, image_data, image_mime_type, product_id, tenant_id)) else: cursor.execute(''' UPDATE products SET name = ?, price = ?, inventory = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - ''', (name, price, inventory, product_id)) + WHERE id = ? AND tenant_id = ? + ''', (name, price, inventory, product_id, tenant_id)) else: # Insert new product cursor.execute(''' - INSERT INTO products (id, name, price, inventory, image_data, image_mime_type) - VALUES (?, ?, ?, ?, ?, ?) - ''', (product_id, name, price, inventory, image_data, image_mime_type)) + INSERT INTO products (id, tenant_id, name, price, inventory, image_data, image_mime_type) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (product_id, tenant_id, name, price, inventory, image_data, image_mime_type)) # Delete existing fields - cursor.execute('DELETE FROM product_fields WHERE product_id = ?', (product_id,)) + cursor.execute('DELETE FROM product_fields WHERE product_id = ? AND tenant_id = ?', (product_id, tenant_id)) # Insert all fields for field in fields: cursor.execute(''' INSERT INTO product_fields - (product_id, field_name, label, value, editable, field_type) - VALUES (?, ?, ?, ?, ?, ?) + (product_id, tenant_id, field_name, label, value, editable, field_type) + VALUES (?, ?, ?, ?, ?, ?, ?) ''', ( product_id, + tenant_id, field['fieldName'], field['label'], field['value'], @@ -249,20 +285,81 @@ def save_product(product_id: str, fields: List[Dict[str, Any]], image_data: Opti conn.commit() -def delete_product(product_id: str): - """Delete a product""" +def delete_product(product_id: str, tenant_id: str): + """Delete a product for a tenant""" with get_db() as conn: cursor = conn.cursor() - cursor.execute('DELETE FROM products WHERE id = ?', (product_id,)) + cursor.execute('DELETE FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) conn.commit() -def get_product_image(product_id: str) -> Optional[tuple[bytes, str]]: - """Get product image data and mime type""" +def get_product_image(product_id: str, tenant_id: str) -> Optional[tuple[bytes, str]]: + """Get product image data and mime type for a tenant""" with get_db() as conn: cursor = conn.cursor() - cursor.execute('SELECT image_data, image_mime_type FROM products WHERE id = ?', (product_id,)) + cursor.execute('SELECT image_data, image_mime_type FROM products WHERE id = ? AND tenant_id = ?', (product_id, tenant_id)) row = cursor.fetchone() if row and row['image_data']: return row['image_data'], row['image_mime_type'] - return None \ No newline at end of file + return None + +def get_or_create_tenant(tenant_id: str, username: str = None, password: str = None) -> Dict[str, Any]: + """Get or create a tenant""" + with get_db() as conn: + cursor = conn.cursor() + + # Check if tenant exists + cursor.execute('SELECT id, name, username, password FROM tenants WHERE id = ?', (tenant_id,)) + row = cursor.fetchone() + + if row: + return { + 'id': row['id'], + 'name': row['name'], + 'username': row['username'], + 'password': row['password'] + } + else: + # Create new tenant with default credentials + default_username = username or 'admin' + default_password = password or 'admin' + cursor.execute(''' + INSERT INTO tenants (id, name, username, password) + VALUES (?, ?, ?, ?) + ''', (tenant_id, tenant_id.title(), default_username, default_password)) + conn.commit() + + return { + 'id': tenant_id, + 'name': tenant_id.title(), + 'username': default_username, + 'password': default_password + } + +def update_tenant_credentials(tenant_id: str, username: str, password: str): + """Update tenant credentials""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE tenants + SET username = ?, password = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (username, password, tenant_id)) + conn.commit() + +def get_all_tenants() -> List[Dict[str, Any]]: + """Get all tenants""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id, name, username, created_at FROM tenants ORDER BY created_at DESC') + + tenants = [] + for row in cursor.fetchall(): + tenants.append({ + 'id': row['id'], + 'name': row['name'], + 'username': row['username'], + 'created_at': row['created_at'] + }) + + return tenants \ No newline at end of file diff --git a/src/routes/admin.py b/src/routes/admin.py index fd862e3..cf49501 100644 --- a/src/routes/admin.py +++ b/src/routes/admin.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file +from flask import render_template, request, redirect, url_for, flash, jsonify, send_file import os import json import uuid @@ -19,8 +19,6 @@ except ImportError: def check_dependencies(): return {'qrcode': False, 'barcode': False, 'pillow': False} -admin_bp = Blueprint('admin', __name__, url_prefix='/admin') - DATA_FOLDER = os.path.join(os.path.dirname(__file__), '../data') PRODUCTS_FILE = os.path.join(DATA_FOLDER, 'products.json') UPLOAD_FOLDER = os.path.join(DATA_FOLDER, 'images') @@ -30,20 +28,19 @@ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS -def load_products(): - return database.get_all_products() +def load_products(tenant_id): + return database.get_all_products(tenant_id) def save_products(products): # This is now handled by database operations pass -@admin_bp.route('/') -def index(): - products = load_products() - return render_template('admin/index.html', products=products) +def index(tenant_id): + products = load_products(tenant_id) + tenant = database.get_or_create_tenant(tenant_id) + return render_template('admin/index.html', products=products, tenant=tenant) -@admin_bp.route('/add', methods=['GET', 'POST']) -def add_product(): +def add_product(tenant_id): if request.method == 'POST': # Get form data product_id = request.form.get('product_id') @@ -53,13 +50,13 @@ def add_product(): # Basic validation if not product_id or not name or not price: flash('Product ID, Name, and Price are required fields.') - return render_template('admin/add_product.html') + return render_template('admin/add_product.html', tenant_id=tenant_id) # Check if product ID already exists - products = load_products() + products = load_products(tenant_id) if product_id in products: flash('Product ID already exists.') - return render_template('admin/add_product.html') + return render_template('admin/add_product.html', tenant_id=tenant_id) # Handle image upload image_data = None @@ -93,20 +90,19 @@ def add_product(): ] # Save to database - database.save_product(product_id, product_data, image_data, image_mime_type) + database.save_product(product_id, tenant_id, product_data, image_data, image_mime_type) flash('Product added successfully!') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) - return render_template('admin/add_product.html') + return render_template('admin/add_product.html', tenant_id=tenant_id) -@admin_bp.route('/edit/', methods=['GET', 'POST']) -def edit_product(product_id): - products = load_products() +def edit_product(tenant_id, product_id): + products = load_products(tenant_id) if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) if request.method == 'POST': # Get form data @@ -116,7 +112,7 @@ def edit_product(product_id): # Basic validation if not name or not price: flash('Name and Price are required fields.') - return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id]) + return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) # Update name and price for field in products[product_id]: @@ -152,41 +148,38 @@ def edit_product(product_id): field["value"] = image_path # Save to database - database.save_product(product_id, products[product_id], image_data, image_mime_type) + database.save_product(product_id, tenant_id, products[product_id], image_data, image_mime_type) flash('Product updated successfully!') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) - return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id]) + return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) -@admin_bp.route('/delete/', methods=['POST']) -def delete_product(product_id): - products = load_products() +def delete_product(tenant_id, product_id): + products = load_products(tenant_id) if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) # Delete product from database (image is stored in DB) - database.delete_product(product_id) + database.delete_product(product_id, tenant_id) flash('Product deleted successfully!') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) -@admin_bp.route('/view/') -def view_product(product_id): - products = load_products() +def view_product(tenant_id, product_id): + products = load_products(tenant_id) if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) - return render_template('admin/view_product.html', product_id=product_id, product=products[product_id]) + return render_template('admin/view_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) -@admin_bp.route('/generate_barcode//') -def generate_barcode(product_id, code_type): +def generate_barcode(tenant_id, product_id, code_type): """Redirect to the main barcode generation endpoint""" - products = load_products() + products = load_products(tenant_id) if product_id not in products: return jsonify({"error": "Product not found"}), 404 @@ -200,16 +193,15 @@ def generate_barcode(product_id, code_type): barcode_type = type_mapping.get(code_type, code_type) # Redirect to the main barcode endpoint which generates dynamically - return redirect(f'/barcodes/{product_id}_{barcode_type}.png') + return redirect(f'/{tenant_id}/barcodes/{product_id}_{barcode_type}.png') -@admin_bp.route('/generate_barcode_page/') -def generate_barcode_page(product_id): +def generate_barcode_page(tenant_id, product_id): """Show a page with different barcode options for a product""" - products = load_products() + products = load_products(tenant_id) if product_id not in products: flash('Product not found.') - return redirect(url_for('admin.index')) + return redirect(url_for('admin.index', tenant_id=tenant_id)) # Extract product data product_data = {} @@ -234,4 +226,22 @@ def generate_barcode_page(product_id): return render_template('admin/generate_barcode.html', product_id=product_id, product=product_data, - dependencies=dependency_status) + dependencies=dependency_status, + tenant_id=tenant_id) + +def manage_credentials(tenant_id): + """Manage tenant login credentials""" + tenant = database.get_or_create_tenant(tenant_id) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + + if username and password: + database.update_tenant_credentials(tenant_id, username, password) + flash('Credentials updated successfully!') + return redirect(url_for('admin.index', tenant_id=tenant_id)) + else: + flash('Username and password are required.') + + return render_template('admin/credentials.html', tenant=tenant, tenant_id=tenant_id) diff --git a/src/routes/api.py b/src/routes/api.py index 8014fa2..285d1c8 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -1,8 +1,5 @@ -# API routes blueprint scaffold -from flask import Blueprint, jsonify +# API routes scaffold +from flask import jsonify -api_bp = Blueprint('api', __name__, url_prefix='/api') - -@api_bp.route('/') -def api_index(): - return jsonify({'message': 'API Home'}) +def api_index(tenant_id): + return jsonify({'message': 'API Home', 'tenant': tenant_id}) diff --git a/src/templates/admin/add_product.html b/src/templates/admin/add_product.html index 41a2c7d..2efc1fa 100644 --- a/src/templates/admin/add_product.html +++ b/src/templates/admin/add_product.html @@ -10,7 +10,7 @@

Add New Product

-
+
@@ -47,7 +47,7 @@
- Cancel + Cancel
diff --git a/src/templates/admin/credentials.html b/src/templates/admin/credentials.html new file mode 100644 index 0000000..63984ff --- /dev/null +++ b/src/templates/admin/credentials.html @@ -0,0 +1,92 @@ + + + + + + Manage Credentials - {{ tenant.name }} + + + + + +
+

Manage Login Credentials

+

These credentials will be used for KCAP authentication at /{{ tenant_id }}/login

+ + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+
+

Current Credentials

+
+
+

Username: {{ tenant.username }}

+

Password: {{ tenant.password }}

+

Note: In production, passwords should be encrypted and not displayed.

+
+
+
+
+
+
+

Update Credentials

+
+
+
+
+ + +
+
+ + +
+ + Cancel +
+
+
+
+
+ +
+

Testing Instructions

+

To test the login endpoint with these credentials:

+
curl -u {{ tenant.username }}:{{ tenant.password }} http://{{ request.host }}/{{ tenant_id }}/login
+
+
+ + + + \ No newline at end of file diff --git a/src/templates/admin/edit_product.html b/src/templates/admin/edit_product.html index 19b9264..f085b0e 100644 --- a/src/templates/admin/edit_product.html +++ b/src/templates/admin/edit_product.html @@ -10,7 +10,7 @@

Edit Product

-
+
@@ -65,7 +65,7 @@
- Cancel + Cancel
diff --git a/src/templates/admin/generate_barcode.html b/src/templates/admin/generate_barcode.html index db9824c..0615bf5 100644 --- a/src/templates/admin/generate_barcode.html +++ b/src/templates/admin/generate_barcode.html @@ -8,7 +8,7 @@

Generate Barcodes for Product: {{ product_id }}

- Back to Products + Back to Products
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %} @@ -38,9 +38,9 @@

QR Code

- QR Code + QR Code

QR Code contains a link to the product AR info.

- Download QR Code + Download QR Code
@@ -51,9 +51,9 @@

EAN-13 Barcode

- EAN-13 Barcode + EAN-13 Barcode

Standard EAN-13 barcode format.

- Download EAN-13 + Download EAN-13
@@ -64,9 +64,9 @@

Code 128 Barcode

- Code 128 Barcode + Code 128 Barcode

High-density alphanumeric barcode.

- Download Code 128 + Download Code 128
diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index f0b1c0b..52079f8 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -1,13 +1,20 @@ {% extends "layout.html" %} -{% block title %}Admin Dashboard - KCAP Demo Server{% endblock %} +{% block title %}Admin Dashboard - {{ tenant.name }} - KCAP Demo Server{% endblock %} {% block content %}
+
+ Current Tenant: {{ tenant.name }} (ID: {{ tenant.id }}) + +

Product Management

- Add New Product + Add New Product
{% if products %} @@ -50,8 +57,8 @@ - diff --git a/src/templates/index.html b/src/templates/index.html index ebbb744..86cc56a 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -6,7 +6,7 @@
-

KCAP Demo Server

+

KCAP Demo Server{% if tenant %} - {{ tenant.name }}{% endif %}

A simple Flask API for demonstrating AR content retrieval for barcode scanning applications.


This server simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes.

@@ -14,25 +14,29 @@

API Endpoints

  • - Login: GET /login -

    Simulates a simple login with a 200 response (no authentication required).

    + Login: GET {% if tenant %}/{{ tenant.id }}{% endif %}/login +

    Authenticates using Basic Auth with tenant-specific credentials.

  • - Content Fields: GET /arcontentfields + Content Fields: GET {% if tenant %}/{{ tenant.id }}{% endif %}/arcontentfields

    Returns a list of available attributes (e.g., item ID, price, image URI).

  • - AR Info: GET /arinfo?barcode=123456 + AR Info: GET {% if tenant %}/{{ tenant.id }}{% endif %}/arinfo?barcode=123456

    Returns product details for a given barcode, including image URLs.

  • - Static Image Server: GET /images/123456.png -

    Serves image files from the static/images/ directory.

    + Static Image Server: GET {% if tenant %}/{{ tenant.id }}{% endif %}/images/123456.png +

    Serves product images stored in the database.

+ {% if tenant %} + Go to Admin Interface + {% else %} Go to Admin Interface + {% endif %}
diff --git a/src/templates/tenant_selection.html b/src/templates/tenant_selection.html new file mode 100644 index 0000000..1cb4c5f --- /dev/null +++ b/src/templates/tenant_selection.html @@ -0,0 +1,75 @@ + + + + + + Select or Create Tenant - KCAP Demo Server + + + +
+

KCAP Demo Server - Multi-Tenant

+ +
+
+
+
+

Select an Existing Tenant

+
+
+ {% if tenants %} + + {% else %} +

No tenants exist yet. Create one below!

+ {% endif %} +
+
+ +
+
+

Create a New Tenant

+
+
+
+
+ + + Only letters, numbers, hyphens, and underscores allowed +
+ +
+
+
+ +
+

+ Note: When you access a tenant URL directly (e.g., /my-tenant/), + it will be automatically created with default credentials (admin/admin). +

+
+
+
+
+ + + + \ No newline at end of file