Major architectural refactoring to improve code organization, maintainability, and scalability: - Implement MVC pattern with clear separation of concerns - Organize code into blueprints for different functional areas (main, tenant, admin, api) - Create models layer to encapsulate all database operations - Create services layer for business logic (auth, product, barcode) - Implement application factory pattern for better testability - Add environment-based configuration management - Remove old monolithic app.py and consolidate routes New structure: - app/models/ - Database operations (Tenant, Product, ARField, Settings) - app/services/ - Business logic (Auth, Product, Barcode services) - app/blueprints/ - Controllers organized by function - app/config.py - Environment configurations - run.py - Application entry point All routes remain backward compatible. No changes to API or templates functionality.
140 lines
7.5 KiB
HTML
140 lines
7.5 KiB
HTML
<!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">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
</head>
|
|
<body>
|
|
<div class="container mt-5">
|
|
<h1 class="text-center mb-4">KCAP Demo Server - Multi-Tenant</h1>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<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 %}
|
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
<a href="/{{ tenant.id }}/" class="text-decoration-none flex-grow-1">
|
|
<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>
|
|
<p class="mb-1">
|
|
<strong>Knox Capture AR Template URL:</strong>
|
|
<code>{{ server_url }}/{{ tenant.id }}/</code>
|
|
</p>
|
|
<small>ID: {{ tenant.id }}</small>
|
|
</a>
|
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#qrModal-{{ tenant.id }}" onclick="event.stopPropagation();">
|
|
<i class="bi bi-qr-code"></i>
|
|
</button>
|
|
<form method="POST" action="/tenant/{{ tenant.id }}/delete" class="ms-3" onsubmit="return confirmDelete('{{ tenant.name }}')">
|
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
|
</form>
|
|
</div>
|
|
{% 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>
|
|
<a href="/settings" class="btn btn-secondary mt-3">Server Settings</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QR Code Modals for each tenant -->
|
|
{% for tenant in tenants %}
|
|
<div class="modal fade" id="qrModal-{{ tenant.id }}" tabindex="-1" aria-labelledby="qrModalLabel-{{ tenant.id }}" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="qrModalLabel-{{ tenant.id }}">AR Template URL QR Code - {{ tenant.name }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<p class="text-muted mb-3">Scan this QR code for the Knox Capture AR Template URL:</p>
|
|
<div class="mb-3">
|
|
<img src="/{{ tenant.id }}/qrcode/template" alt="AR Template QR Code" class="img-fluid" style="max-width: 300px;">
|
|
</div>
|
|
<code class="d-block mt-2">{{ server_url }}/{{ tenant.id }}/</code>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const reservedIds = ['admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
|
|
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
|
|
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'];
|
|
|
|
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const tenantId = document.getElementById('tenantId').value;
|
|
if (tenantId) {
|
|
// Convert to lowercase for consistency
|
|
const normalizedId = tenantId.toLowerCase();
|
|
if (reservedIds.includes(normalizedId)) {
|
|
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
|
|
return;
|
|
}
|
|
// Use the normalized (lowercase) ID in the URL
|
|
window.location.href = '/' + normalizedId + '/';
|
|
}
|
|
});
|
|
|
|
function confirmDelete(tenantName) {
|
|
return confirm(`Are you sure you want to delete the tenant "${tenantName}"?\n\nThis will permanently delete all products and data associated with this tenant.`);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |