ability to delete a tenant
This commit is contained in:
17
src/app.py
17
src/app.py
@@ -30,10 +30,20 @@ def index():
|
|||||||
tenants = database.get_all_tenants()
|
tenants = database.get_all_tenants()
|
||||||
return render_template('tenant_selection.html', tenants=tenants)
|
return render_template('tenant_selection.html', tenants=tenants)
|
||||||
|
|
||||||
|
@app.route('/tenant/<tenant:tenant_id>/delete', methods=['POST'])
|
||||||
|
def delete_tenant(tenant_id):
|
||||||
|
"""Delete a tenant and all its data"""
|
||||||
|
database.delete_tenant(tenant_id)
|
||||||
|
flash(f'Tenant "{tenant_id}" has been deleted successfully.', 'success')
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
@app.route('/<tenant:tenant_id>/')
|
@app.route('/<tenant:tenant_id>/')
|
||||||
def tenant_index(tenant_id):
|
def tenant_index(tenant_id):
|
||||||
# Auto-create tenant if it doesn't exist
|
# Auto-create tenant if it doesn't exist
|
||||||
tenant = database.get_or_create_tenant(tenant_id)
|
tenant = database.get_or_create_tenant(tenant_id)
|
||||||
|
if tenant is None:
|
||||||
|
# Reserved tenant ID or invalid
|
||||||
|
return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404
|
||||||
return render_template('index.html', tenant=tenant)
|
return render_template('index.html', tenant=tenant)
|
||||||
|
|
||||||
def check_basic_auth(auth_header, tenant_id):
|
def check_basic_auth(auth_header, tenant_id):
|
||||||
@@ -48,7 +58,7 @@ def check_basic_auth(auth_header, tenant_id):
|
|||||||
|
|
||||||
# Get tenant credentials from database
|
# Get tenant credentials from database
|
||||||
tenant = database.get_or_create_tenant(tenant_id)
|
tenant = database.get_or_create_tenant(tenant_id)
|
||||||
if username == tenant['username'] and password == tenant['password']:
|
if tenant and username == tenant['username'] and password == tenant['password']:
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -228,4 +238,9 @@ if __name__ == '__main__':
|
|||||||
shutil.move(products_file, products_file + '.migrated')
|
shutil.move(products_file, products_file + '.migrated')
|
||||||
print("Migration complete.")
|
print("Migration complete.")
|
||||||
|
|
||||||
|
# Clean up any accidentally created reserved tenants
|
||||||
|
deleted_count = database.cleanup_reserved_tenants()
|
||||||
|
if deleted_count > 0:
|
||||||
|
print(f"Cleaned up {deleted_count} reserved tenant(s)")
|
||||||
|
|
||||||
app.run(port=5555, host="0.0.0.0", debug=True)
|
app.run(port=5555, host="0.0.0.0", debug=True)
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import base64
|
|||||||
|
|
||||||
DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'products.db')
|
DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'products.db')
|
||||||
|
|
||||||
|
# Reserved tenant IDs that cannot be used
|
||||||
|
RESERVED_TENANT_IDS = {
|
||||||
|
'admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
|
||||||
|
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
|
||||||
|
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'
|
||||||
|
}
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_db():
|
def get_db():
|
||||||
"""Context manager for database connections"""
|
"""Context manager for database connections"""
|
||||||
@@ -303,8 +310,12 @@ def get_product_image(product_id: str, tenant_id: str) -> Optional[tuple[bytes,
|
|||||||
return row['image_data'], row['image_mime_type']
|
return row['image_data'], row['image_mime_type']
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_or_create_tenant(tenant_id: str, username: str = None, password: str = None) -> Dict[str, Any]:
|
def get_or_create_tenant(tenant_id: str, username: str = None, password: str = None) -> Optional[Dict[str, Any]]:
|
||||||
"""Get or create a tenant"""
|
"""Get or create a tenant"""
|
||||||
|
# Check if tenant_id is reserved
|
||||||
|
if tenant_id.lower() in RESERVED_TENANT_IDS:
|
||||||
|
return None
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
@@ -363,3 +374,30 @@ def get_all_tenants() -> List[Dict[str, Any]]:
|
|||||||
})
|
})
|
||||||
|
|
||||||
return tenants
|
return tenants
|
||||||
|
|
||||||
|
def delete_tenant(tenant_id: str):
|
||||||
|
"""Delete a tenant and all associated data"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Due to ON DELETE CASCADE, this will also delete all products and product_fields
|
||||||
|
cursor.execute('DELETE FROM tenants WHERE id = ?', (tenant_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def cleanup_reserved_tenants():
|
||||||
|
"""Remove any tenants that were accidentally created with reserved IDs"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Get all tenants
|
||||||
|
cursor.execute('SELECT id FROM tenants')
|
||||||
|
tenants = cursor.fetchall()
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
for tenant in tenants:
|
||||||
|
tenant_id = tenant['id']
|
||||||
|
if tenant_id.lower() in RESERVED_TENANT_IDS:
|
||||||
|
cursor.execute('DELETE FROM tenants WHERE id = ?', (tenant_id,))
|
||||||
|
deleted_count += 1
|
||||||
|
print(f"Deleted reserved tenant: {tenant_id}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return deleted_count
|
||||||
@@ -10,6 +10,17 @@
|
|||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<h1 class="text-center mb-4">KCAP Demo Server - Multi-Tenant</h1>
|
<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="row justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -20,14 +31,19 @@
|
|||||||
{% if tenants %}
|
{% if tenants %}
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{% for tenant in tenants %}
|
{% for tenant in tenants %}
|
||||||
<a href="/{{ tenant.id }}/" class="list-group-item list-group-item-action">
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<a href="/{{ tenant.id }}/" class="text-decoration-none flex-grow-1">
|
||||||
<h5 class="mb-1">{{ tenant.name }}</h5>
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<small>{{ tenant.created_at }}</small>
|
<h5 class="mb-1">{{ tenant.name }}</h5>
|
||||||
</div>
|
<small>{{ tenant.created_at }}</small>
|
||||||
<p class="mb-1">Username: {{ tenant.username }}</p>
|
</div>
|
||||||
<small>ID: {{ tenant.id }}</small>
|
<p class="mb-1">Username: {{ tenant.username }}</p>
|
||||||
</a>
|
<small>ID: {{ tenant.id }}</small>
|
||||||
|
</a>
|
||||||
|
<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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -62,14 +78,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<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) {
|
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const tenantId = document.getElementById('tenantId').value;
|
const tenantId = document.getElementById('tenantId').value;
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
|
if (reservedIds.includes(tenantId.toLowerCase())) {
|
||||||
|
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.location.href = '/' + tenantId + '/';
|
window.location.href = '/' + tenantId + '/';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user