Browse Source

Add empty-only Remove Device flow

master
Don Kingdon 3 weeks ago
parent
commit
b87431f88d
  1. 4
      PROJECT_STATE.md
  2. 7
      README.md
  3. 2
      VERSION
  4. 7
      app/auth/utils.py
  5. 66
      app/main/routes.py
  6. 9
      app/templates/cloud/dashboard.html

4
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.

7
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

2
VERSION

@ -1 +1 @@
v0.1.3
v0.1.4

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

66
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/<int:device_id>", 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"))

9
app/templates/cloud/dashboard.html

@ -54,12 +54,15 @@
</div>
<div class="service-card-actions">
<ul style="padding-left:18px; margin:0;">
<ul style="padding-left:18px; margin:0; list-style:disc;">
{% for device in devices %}
<li style="margin-bottom:10px;">
<li style="margin-bottom:18px;">
<strong>{{ device.device_name }}</strong>
<span style="opacity:0.85;">({{ device.device_type }})</span><br>
<span style="opacity:0.75;">{{ device.relative_path }}</span>
<span style="opacity:0.75;">{{ device.relative_path }}</span><br>
<form method="post" action="{{ url_for('main.delete_device', device_id=device.id) }}" style="margin-top:10px;">
<button class="portal-btn" type="submit" onclick="return confirm('Remove device {{ device.device_name|e }}? This only works if no files are linked to it.');">Remove Device</button>
</form>
</li>
{% endfor %}
</ul>

Loading…
Cancel
Save