made multi tenant
This commit is contained in:
102
src/app.py
102
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('/<tenant:tenant_id>/')
|
||||
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('/<tenant:tenant_id>/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('/<tenant:tenant_id>/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('/<tenant:tenant_id>/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/<path:filename>', methods=['GET'])
|
||||
def serve_image(filename):
|
||||
@app.route('/<tenant:tenant_id>/images/<path:filename>', 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/<path:filename>', methods=['GET'])
|
||||
def serve_barcode(filename):
|
||||
@app.route('/<tenant:tenant_id>/barcodes/<path:filename>', 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('/<tenant:tenant_id>/admin/', 'admin.index', admin_index)
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/add', 'admin.add_product', add_product, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/edit/<product_id>', 'admin.edit_product', edit_product, methods=['GET', 'POST'])
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/delete/<product_id>', 'admin.delete_product', delete_product, methods=['POST'])
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/view/<product_id>', 'admin.view_product', view_product)
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/generate_barcode/<product_id>/<code_type>', 'admin.generate_barcode', generate_barcode)
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/generate_barcode_page/<product_id>', 'admin.generate_barcode_page', generate_barcode_page)
|
||||
app.add_url_rule('/<tenant:tenant_id>/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('/<tenant:tenant_id>/api/', 'api.api_index', api_index)
|
||||
|
||||
# Catch-all route for admin without tenant - redirect to home
|
||||
@app.route('/admin/')
|
||||
@app.route('/admin/<path:path>')
|
||||
def redirect_admin_to_home(path=None):
|
||||
return redirect('/')
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Initialize database
|
||||
database.init_database()
|
||||
|
||||
177
src/database.py
177
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,18 +153,29 @@ 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
|
||||
# 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
|
||||
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
|
||||
@@ -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
|
||||
|
||||
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
|
||||
@@ -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/<product_id>', 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/<product_id>', 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/<product_id>')
|
||||
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/<product_id>/<code_type>')
|
||||
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/<product_id>')
|
||||
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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2>Add New Product</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('admin.add_product') }}" method="POST" enctype="multipart/form-data">
|
||||
<form action="{{ url_for('admin.add_product', tenant_id=tenant_id) }}" method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="product_id" class="form-label">Product ID *</label>
|
||||
<div class="input-group">
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Add Product</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
92
src/templates/admin/credentials.html
Normal file
92
src/templates/admin/credentials.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage Credentials - {{ tenant.name }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('admin.index', tenant_id=tenant_id) }}">{{ tenant.name }} Admin</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('admin.index', tenant_id=tenant_id) }}">Products</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{{ url_for('admin.manage_credentials', tenant_id=tenant_id) }}">Credentials</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Switch Tenant</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5">
|
||||
<h1>Manage Login Credentials</h1>
|
||||
<p class="text-muted">These credentials will be used for KCAP authentication at <code>/{{ tenant_id }}/login</code></p>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Current Credentials</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Username:</strong> {{ tenant.username }}</p>
|
||||
<p><strong>Password:</strong> {{ tenant.password }}</p>
|
||||
<p class="text-muted small">Note: In production, passwords should be encrypted and not displayed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Update Credentials</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" value="{{ tenant.username }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter new password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Update Credentials</button>
|
||||
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3>Testing Instructions</h3>
|
||||
<p>To test the login endpoint with these credentials:</p>
|
||||
<pre class="bg-light p-3"><code>curl -u {{ tenant.username }}:{{ tenant.password }} http://{{ request.host }}/{{ tenant_id }}/login</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2>Edit Product</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('admin.edit_product', product_id=product_id) }}" method="POST" enctype="multipart/form-data">
|
||||
<form action="{{ url_for('admin.edit_product', tenant_id=tenant_id, product_id=product_id) }}" method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="product_id" class="form-label">Product ID</label>
|
||||
<input type="text" class="form-control" id="product_id" value="{{ product_id }}" disabled>
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Update Product</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Generate Barcodes for Product: {{ product_id }}</h2>
|
||||
<a href="{{ url_for('admin.index') }}" class="btn btn-outline-secondary">Back to Products</a>
|
||||
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-outline-secondary">Back to Products</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %}
|
||||
@@ -38,9 +38,9 @@
|
||||
<h3>QR Code</h3>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='qrcode') }}" alt="QR Code" class="img-fluid mb-3" style="max-width: 250px;">
|
||||
<img src="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='qrcode') }}" alt="QR Code" class="img-fluid mb-3" style="max-width: 250px;">
|
||||
<p class="text-muted">QR Code contains a link to the product AR info.</p>
|
||||
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='qrcode') }}" class="btn btn-primary" download="product_{{ product_id }}_qrcode.png">Download QR Code</a>
|
||||
<a href="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='qrcode') }}" class="btn btn-primary" download="product_{{ product_id }}_qrcode.png">Download QR Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,9 +51,9 @@
|
||||
<h3>EAN-13 Barcode</h3>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='ean13') }}" alt="EAN-13 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
|
||||
<img src="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='ean13') }}" alt="EAN-13 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
|
||||
<p class="text-muted">Standard EAN-13 barcode format.</p>
|
||||
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='ean13') }}" class="btn btn-primary" download="product_{{ product_id }}_ean13.png">Download EAN-13</a>
|
||||
<a href="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='ean13') }}" class="btn btn-primary" download="product_{{ product_id }}_ean13.png">Download EAN-13</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,9 +64,9 @@
|
||||
<h3>Code 128 Barcode</h3>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='code128') }}" alt="Code 128 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
|
||||
<img src="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='code128') }}" alt="Code 128 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
|
||||
<p class="text-muted">High-density alphanumeric barcode.</p>
|
||||
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='code128') }}" class="btn btn-primary" download="product_{{ product_id }}_code128.png">Download Code 128</a>
|
||||
<a href="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='code128') }}" class="btn btn-primary" download="product_{{ product_id }}_code128.png">Download Code 128</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-info">
|
||||
<strong>Current Tenant:</strong> {{ tenant.name }} (ID: {{ tenant.id }})
|
||||
<div class="mt-2">
|
||||
<a href="{{ url_for('admin.manage_credentials', tenant_id=tenant.id) }}" class="btn btn-sm btn-secondary">Manage Credentials</a>
|
||||
<a href="/" class="btn btn-sm btn-outline-secondary">Switch Tenant</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Product Management</h1>
|
||||
<a href="{{ url_for('admin.add_product') }}" class="btn btn-primary">Add New Product</a>
|
||||
<a href="{{ url_for('admin.add_product', tenant_id=tenant.id) }}" class="btn btn-primary">Add New Product</a>
|
||||
</div>
|
||||
|
||||
{% if products %}
|
||||
@@ -50,8 +57,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center gap-2">
|
||||
<a href="{{ url_for('admin.edit_product', product_id=product_id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<a href="{{ url_for('admin.generate_barcode_page', product_id=product_id) }}" class="btn btn-sm btn-outline-success">Barcodes</a>
|
||||
<a href="{{ url_for('admin.edit_product', tenant_id=tenant.id, product_id=product_id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<a href="{{ url_for('admin.generate_barcode_page', tenant_id=tenant.id, product_id=product_id) }}" class="btn btn-sm btn-outline-success">Barcodes</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-product-id="{{ product_id }}"
|
||||
@@ -59,7 +66,7 @@
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<form id="delete-form-{{ product_id }}" action="{{ url_for('admin.delete_product', product_id=product_id) }}" method="POST" style="display: none;">
|
||||
<form id="delete-form-{{ product_id }}" action="{{ url_for('admin.delete_product', tenant_id=tenant.id, product_id=product_id) }}" method="POST" style="display: none;">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="jumbotron">
|
||||
<h1 class="display-4">KCAP Demo Server</h1>
|
||||
<h1 class="display-4">KCAP Demo Server{% if tenant %} - {{ tenant.name }}{% endif %}</h1>
|
||||
<p class="lead">A simple Flask API for demonstrating AR content retrieval for barcode scanning applications.</p>
|
||||
<hr class="my-4">
|
||||
<p>This server simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes.</p>
|
||||
@@ -14,25 +14,29 @@
|
||||
<h2>API Endpoints</h2>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>Login:</strong> <code>GET /login</code>
|
||||
<p>Simulates a simple login with a 200 response (no authentication required).</p>
|
||||
<strong>Login:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/login</code>
|
||||
<p>Authenticates using Basic Auth with tenant-specific credentials.</p>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Content Fields:</strong> <code>GET /arcontentfields</code>
|
||||
<strong>Content Fields:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/arcontentfields</code>
|
||||
<p>Returns a list of available attributes (e.g., item ID, price, image URI).</p>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>AR Info:</strong> <code>GET /arinfo?barcode=123456</code>
|
||||
<strong>AR Info:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/arinfo?barcode=123456</code>
|
||||
<p>Returns product details for a given barcode, including image URLs.</p>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>Static Image Server:</strong> <code>GET /images/123456.png</code>
|
||||
<p>Serves image files from the <code>static/images/</code> directory.</p>
|
||||
<strong>Static Image Server:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/images/123456.png</code>
|
||||
<p>Serves product images stored in the database.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{% if tenant %}
|
||||
<a href="/{{ tenant.id }}/admin/" class="btn btn-primary btn-lg">Go to Admin Interface</a>
|
||||
{% else %}
|
||||
<a href="/admin" class="btn btn-primary btn-lg">Go to Admin Interface</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
75
src/templates/tenant_selection.html
Normal file
75
src/templates/tenant_selection.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Select or Create Tenant - KCAP Demo Server</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="text-center mb-4">KCAP Demo Server - Multi-Tenant</h1>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Select an Existing Tenant</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if tenants %}
|
||||
<div class="list-group">
|
||||
{% for tenant in tenants %}
|
||||
<a href="/{{ tenant.id }}/" class="list-group-item list-group-item-action">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ tenant.name }}</h5>
|
||||
<small>{{ tenant.created_at }}</small>
|
||||
</div>
|
||||
<p class="mb-1">Username: {{ tenant.username }}</p>
|
||||
<small>ID: {{ tenant.id }}</small>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No tenants exist yet. Create one below!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h3>Create a New Tenant</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="newTenantForm">
|
||||
<div class="mb-3">
|
||||
<label for="tenantId" class="form-label">Tenant ID</label>
|
||||
<input type="text" class="form-control" id="tenantId" placeholder="e.g., demo-company" required pattern="[a-zA-Z0-9_-]+">
|
||||
<small class="form-text text-muted">Only letters, numbers, hyphens, and underscores allowed</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create & Enter Tenant</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<p class="text-muted">
|
||||
<strong>Note:</strong> When you access a tenant URL directly (e.g., /my-tenant/),
|
||||
it will be automatically created with default credentials (admin/admin).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const tenantId = document.getElementById('tenantId').value;
|
||||
if (tenantId) {
|
||||
window.location.href = '/' + tenantId + '/';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user