made multi tenant

This commit is contained in:
2025-06-19 14:25:42 -04:00
parent f041266ac3
commit 705f2859fa
11 changed files with 473 additions and 157 deletions

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>