From b87431f88d1e8dd0245a8a5d849b61b09cd1b3cb Mon Sep 17 00:00:00 2001 From: Don Kingdon Date: Mon, 13 Apr 2026 02:10:31 +0000 Subject: [PATCH] Add empty-only Remove Device flow --- PROJECT_STATE.md | 4 +- README.md | 7 ++++ VERSION | 2 +- app/auth/utils.py | 7 ++++ app/main/routes.py | 66 +++++++++++++++++++++++++++++- app/templates/cloud/dashboard.html | 9 ++-- 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index fcad907..9539661 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -4,7 +4,7 @@ OTB Cloud ## Current version -v0.1.3 +v0.1.4 ## Build date 2026-04-12 @@ -48,6 +48,7 @@ Portal-authenticated secure backup and storage platform for customer files, incl - Mintme reverse proxy in place - OTB Billing signed handoff working - Add Device flow +- Remove Device flow for empty devices ## Immediate next tasks 1. Build first file library page @@ -62,3 +63,4 @@ Original uploaded files should remain preserved and effectively read-only. Any user-facing edits or processing outputs should create derivative files. Admin access should require owner-issued one-time support authorization. New tenants no longer receive default devices automatically; devices are now user-created. +Devices can only be removed when no files are associated with them. diff --git a/README.md b/README.md index 0d97089..4a92c56 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # OTB Cloud +## v0.1.4 - 2026-04-12 +- Added Remove Device flow +- Device removal is POST-only +- Devices can only be removed when no files are linked to them +- Added audit logging for device removal +- Added device directory cleanup for empty devices + ## v0.1.3 - 2026-04-12 - Removed automatic default device creation for new tenants - Added real Add Device flow diff --git a/VERSION b/VERSION index 04e1946..8c43fb4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.1.3 +v0.1.4 diff --git a/app/auth/utils.py b/app/auth/utils.py index de359f1..cc6c6b5 100644 --- a/app/auth/utils.py +++ b/app/auth/utils.py @@ -1,6 +1,7 @@ import hashlib import hmac import re +import shutil import time from pathlib import Path @@ -128,3 +129,9 @@ def create_device_directories(tenant_slug: str, device_path: str): base = tenant_root / device_path for subdir in ["originals", "derived", "exports", "deleted", "tmp"]: (base / subdir).mkdir(parents=True, exist_ok=True) + +def remove_device_directories(tenant_slug: str, device_path: str): + tenant_root = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / tenant_slug + base = tenant_root / device_path + if base.exists() and base.is_dir(): + shutil.rmtree(base) diff --git a/app/main/routes.py b/app/main/routes.py index 09a881b..9be4fcb 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -3,7 +3,7 @@ from functools import wraps from flask import Blueprint, flash, redirect, render_template, request, session, url_for from app.db import get_db -from app.auth.utils import create_device_directories, slugify_device_name +from app.auth.utils import create_device_directories, remove_device_directories, slugify_device_name bp = Blueprint("main", __name__) @@ -133,3 +133,67 @@ def add_device(): flash("Device added successfully.", "success") return redirect(url_for("main.dashboard")) + +@bp.route("/devices/delete/", methods=["POST"]) +@portal_session_required +def delete_device(device_id: int): + db = get_db() + + with db.cursor() as cur: + cur.execute( + """ + SELECT id, device_name, device_type, relative_path + FROM devices + WHERE id = %s AND tenant_id = %s + """, + (device_id, session["otb_tenant_id"]), + ) + device = cur.fetchone() + + if not device: + flash("Device not found.", "warning") + return redirect(url_for("main.dashboard")) + + cur.execute( + """ + SELECT COUNT(*) AS file_count + FROM files + WHERE tenant_id = %s AND device_id = %s AND is_deleted = 0 + """, + (session["otb_tenant_id"], device_id), + ) + file_count = cur.fetchone()["file_count"] + + if file_count and int(file_count) > 0: + flash("This device cannot be removed because files are still linked to it.", "warning") + return redirect(url_for("main.dashboard")) + + cur.execute( + """ + DELETE FROM devices + WHERE id = %s AND tenant_id = %s + """, + (device_id, session["otb_tenant_id"]), + ) + + cur.execute( + """ + INSERT INTO audit_logs ( + tenant_id, user_id, actor_type, event_type, ip_address, user_agent, event_detail + ) VALUES (%s, %s, 'user', 'device_deleted', %s, %s, %s) + """, + ( + session["otb_tenant_id"], + session["otb_user_id"], + request.headers.get("X-Forwarded-For", request.remote_addr), + request.headers.get("User-Agent", ""), + f"Deleted device '{device['device_name']}' ({device['device_type']}) at {device['relative_path']}", + ), + ) + + db.commit() + + remove_device_directories(session["otb_tenant_slug"], device["relative_path"]) + + flash("Device removed successfully.", "success") + return redirect(url_for("main.dashboard")) diff --git a/app/templates/cloud/dashboard.html b/app/templates/cloud/dashboard.html index db6fb03..37bdca9 100644 --- a/app/templates/cloud/dashboard.html +++ b/app/templates/cloud/dashboard.html @@ -54,12 +54,15 @@
-
    +
      {% for device in devices %} -
    • +
    • {{ device.device_name }} ({{ device.device_type }})
      - {{ device.relative_path }} + {{ device.relative_path }}
      +
      + +
    • {% endfor %}