From 20a0098b91dfeeea55e7824043d19733f4c65e5d Mon Sep 17 00:00:00 2001 From: Don Kingdon Date: Mon, 13 Apr 2026 01:54:32 +0000 Subject: [PATCH] Replace default devices with user-created Add Device flow --- PROJECT_STATE.md | 37 ++++-------- README.md | 47 +++------------ VERSION | 2 +- app/auth/utils.py | 27 ++++----- app/main/routes.py | 92 ++++++++++++++++++++++++++++- app/templates/cloud/dashboard.html | 75 ++++++++++++++++++----- app/templates/cloud/device_new.html | 89 ++++++++++++++++++++++++++++ 7 files changed, 272 insertions(+), 97 deletions(-) create mode 100644 app/templates/cloud/device_new.html diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index f087ba6..fcad907 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -4,7 +4,7 @@ OTB Cloud ## Current version -v0.1.2 +v0.1.3 ## Build date 2026-04-12 @@ -25,7 +25,7 @@ Portal-authenticated secure backup and storage platform for customer files, incl - MariaDB backend - Vault3 storage root at `/tank/backups/otb-cloud` - Tenant-isolated storage -- Device-defined source directories +- User-created devices - Immutable originals - Derived-file processing workflow - Search by filename and date @@ -33,21 +33,6 @@ Portal-authenticated secure backup and storage platform for customer files, incl - Audit logging - Owner-approved admin support access using one-time token -## Device organization model -Per-tenant storage will be organized by named devices, for example: -- laptop -- phone -- tablet -- workpc -- homepc - -Each device should have: -- originals/ -- derived/ -- exports/ -- deleted/ -- tmp/ - ## Current implemented scaffold - Flask app factory - Main blueprint @@ -55,23 +40,25 @@ Each device should have: - MariaDB connection helper - Signed handoff endpoint - Auth-protected dashboard -- Temporary portal base template +- Branded OTB portal shell styling - SQL schema file - DB bootstrap script - Storage bootstrap scripts - Gunicorn systemd service on vault3 - Mintme reverse proxy in place +- OTB Billing signed handoff working +- Add Device flow ## Immediate next tasks -1. Patch OTB Billing to add OTB Cloud services card -2. Add signed handoff redirect route in OTB Billing -3. Replace temporary portal base with shared portal template structure -4. Build file library and upload endpoints -5. Add upload audit logging -6. Add first real storage browsing page +1. Build first file library page +2. Add upload endpoint and upload form +3. Add upload audit logging +4. Add zip export flow +5. Add searchable file listing +6. Add media processing jobs ## Notes 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. -This version cleans up the temporary UI while keeping the same signed handoff endpoint for OTB Billing integration. +New tenants no longer receive default devices automatically; devices are now user-created. diff --git a/README.md b/README.md index cee613f..0d97089 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # OTB Cloud +## v0.1.3 - 2026-04-12 +- Removed automatic default device creation for new tenants +- Added real Add Device flow +- Updated dashboard to show empty state when no devices exist +- Device directories are now created only when a user adds a device + ## v0.1.2 - 2026-04-12 - Cleaned up unauthenticated OTB Cloud pages - Hid Dashboard/Logout navigation when not authenticated @@ -32,49 +38,10 @@ OTB Cloud is a private portal-authenticated backup and storage platform for Outs Primary goals: - Secure backup and storage for documents, images, videos, and uploaded files - Per-customer tenant isolation -- Device-based organization (laptop, phone, tablet, workpc, homepc, etc.) +- Device-based organization - Immutable original uploads - Derived file workflow for processing and edits - Searchable file library - Bulk upload and bulk export support - Audit logging - Owner-approved admin support access using one-time token workflow - -## Planned host and path -- Host: vault3 -- App path: `/opt/otb_cloud` -- Domain: `otb-cloud.outsidethebox.top` -- Storage root: `/tank/backups/otb-cloud` - -## Planned backend stack -- Flask -- MariaDB -- Jinja templates with shared portal base -- Background job processing for media conversions -- FFmpeg for video/audio processing -- Nginx reverse proxy - -## Security goals -- Portal-authenticated access only -- No unauthenticated file access -- Tenant-isolated storage and database access -- Audit logging for login attempts and file actions -- Encrypted storage at rest -- HTTPS/TLS in transit -- Immutable originals by default - -## Device model -Each tenant may define logical upload devices such as: -- laptop -- phone -- tablet -- workpc -- homepc - -Uploads are organized by source device to preserve context. - -## Documentation policy -This repository uses: -- `README.md` for version/change log summary -- `PROJECT_STATE.md` for current status and working notes -- `VERSION` for current version diff --git a/VERSION b/VERSION index 5366600..04e1946 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.1.2 +v0.1.3 diff --git a/app/auth/utils.py b/app/auth/utils.py index 29957f7..de359f1 100644 --- a/app/auth/utils.py +++ b/app/auth/utils.py @@ -32,6 +32,9 @@ def slugify_email(email: str) -> str: slug = SLUG_RE.sub("-", local).strip("-") return slug or "tenant" +def slugify_device_name(name: str) -> str: + return SLUG_RE.sub("-", name.lower().strip()).strip("-") or "device" + def ensure_user_tenant_and_devices(email: str, portal_user_id: int): db = get_db() @@ -98,25 +101,14 @@ def ensure_user_tenant_and_devices(email: str, portal_user_id: int): (user_id, slug, storage_root), ) tenant_id = cur.lastrowid + create_tenant_root(slug) else: tenant_id = tenant["id"] slug = tenant["slug"] storage_root = tenant["storage_root"] - for device_name in current_app.config["DEFAULT_DEVICE_NAMES"]: - relative_path = f"devices/{device_name}" - cur.execute( - """ - INSERT IGNORE INTO devices (tenant_id, device_name, device_type, relative_path, is_active) - VALUES (%s, %s, %s, %s, 1) - """, - (tenant_id, device_name, device_name, relative_path), - ) - db.commit() - create_tenant_directories(slug, current_app.config["DEFAULT_DEVICE_NAMES"]) - return { "user_id": user_id, "tenant_id": tenant_id, @@ -125,11 +117,14 @@ def ensure_user_tenant_and_devices(email: str, portal_user_id: int): "email": email, } -def create_tenant_directories(tenant_slug: str, device_names: list[str]): +def create_tenant_root(tenant_slug: str): tenant_root = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / tenant_slug (tenant_root / "logs").mkdir(parents=True, exist_ok=True) (tenant_root / "support").mkdir(parents=True, exist_ok=True) + (tenant_root / "devices").mkdir(parents=True, exist_ok=True) - for device_name in device_names: - for subdir in ["originals", "derived", "exports", "deleted", "tmp"]: - (tenant_root / "devices" / device_name / subdir).mkdir(parents=True, exist_ok=True) +def create_device_directories(tenant_slug: str, device_path: str): + tenant_root = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / tenant_slug + base = tenant_root / device_path + for subdir in ["originals", "derived", "exports", "deleted", "tmp"]: + (base / subdir).mkdir(parents=True, exist_ok=True) diff --git a/app/main/routes.py b/app/main/routes.py index 38569a0..09a881b 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,8 +1,9 @@ from functools import wraps -from flask import Blueprint, redirect, render_template, session, url_for +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 bp = Blueprint("main", __name__) @@ -43,3 +44,92 @@ def dashboard(): tenant_slug=session.get("otb_tenant_slug"), devices=devices, ) + +@bp.route("/devices/new", methods=["GET", "POST"]) +@portal_session_required +def add_device(): + if request.method == "GET": + return render_template( + "cloud/device_new.html", + user_email=session.get("otb_email"), + tenant_slug=session.get("otb_tenant_slug"), + ) + + device_name = (request.form.get("device_name") or "").strip() + device_type = (request.form.get("device_type") or "").strip() + + if not device_name: + flash("Device name is required.", "warning") + return render_template( + "cloud/device_new.html", + user_email=session.get("otb_email"), + tenant_slug=session.get("otb_tenant_slug"), + device_name=device_name, + device_type=device_type, + ) + + if not device_type: + flash("Device type is required.", "warning") + return render_template( + "cloud/device_new.html", + user_email=session.get("otb_email"), + tenant_slug=session.get("otb_tenant_slug"), + device_name=device_name, + device_type=device_type, + ) + + slug = slugify_device_name(device_name) + relative_path = f"devices/{slug}" + + db = get_db() + + with db.cursor() as cur: + cur.execute( + """ + SELECT id + FROM devices + WHERE tenant_id = %s AND device_name = %s + """, + (session["otb_tenant_id"], device_name), + ) + existing = cur.fetchone() + + if existing: + flash("A device with that name already exists.", "warning") + return render_template( + "cloud/device_new.html", + user_email=session.get("otb_email"), + tenant_slug=session.get("otb_tenant_slug"), + device_name=device_name, + device_type=device_type, + ) + + cur.execute( + """ + INSERT INTO devices (tenant_id, device_name, device_type, relative_path, is_active) + VALUES (%s, %s, %s, %s, 1) + """, + (session["otb_tenant_id"], device_name, device_type, relative_path), + ) + + 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_created', %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"Created device '{device_name}' ({device_type}) at {relative_path}", + ), + ) + + db.commit() + + create_device_directories(session["otb_tenant_slug"], relative_path) + + flash("Device added successfully.", "success") + return redirect(url_for("main.dashboard")) diff --git a/app/templates/cloud/dashboard.html b/app/templates/cloud/dashboard.html index a36e9b3..db6fb03 100644 --- a/app/templates/cloud/dashboard.html +++ b/app/templates/cloud/dashboard.html @@ -14,7 +14,8 @@
- Back to Services + Add Device + Back to Services Logout
@@ -27,6 +28,19 @@
+{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ category|capitalize }}: {{ message }} +
+ {% endfor %} +
+ {% endif %} +{% endwith %} + +{% if devices %}
@@ -40,19 +54,15 @@
- {% if devices %} -
    - {% for device in devices %} -
  • - {{ device.device_name }} - ({{ device.device_type }})
    - {{ device.relative_path }} -
  • - {% endfor %} -
- {% else %} -

No devices have been created yet.

- {% endif %} +
    + {% for device in devices %} +
  • + {{ device.device_name }} + ({{ device.device_type }})
    + {{ device.relative_path }} +
  • + {% endfor %} +
@@ -74,4 +84,41 @@
+{% else %} +
+
+
+
+

No devices connected yet

+

Create your first device source before uploading files.

+
+
+ Ready +
+
+ + +
+ +
+
+
+

Current scope

+

OTB Cloud is ready for user-created devices.

+
+
+ In Progress +
+
+ +
+

+ After adding a device, the next phase is browser upload, file library browsing, and export tools. +

+
+
+
+{% endif %} {% endblock %} diff --git a/app/templates/cloud/device_new.html b/app/templates/cloud/device_new.html new file mode 100644 index 0000000..f9a721e --- /dev/null +++ b/app/templates/cloud/device_new.html @@ -0,0 +1,89 @@ +{% extends "portal_base.html" %} + +{% block title %}Add Device - OTB Cloud{% endblock %} + +{% block portal_content %} +
+
+

Add Device

+

{{ user_email }}

+

+ Create a named source for uploads like a laptop, phone, tablet, or workstation. +

+
+ + +
+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ category|capitalize }}: {{ message }} +
+ {% endfor %} +
+ {% endif %} +{% endwith %} + +
+
+
+
+

Device Details

+

This creates a device source and its storage folders.

+
+
+ Setup +
+
+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + Cancel +
+
+
+
+
+
+{% endblock %}