From e8dd3b9f90db6c0c493994738bc038a1c695a03e Mon Sep 17 00:00:00 2001 From: Don Kingdon Date: Sun, 12 Apr 2026 20:33:51 +0000 Subject: [PATCH] Initial OTB Cloud scaffold v0.1.0 --- .env.example | 9 +++ .gitignore | 59 ++++++++++++++++++ PROJECT_STATE.md | 71 ++++++++++++++++++++++ README.md | 64 ++++++++++++++++++++ VERSION | 1 + app/__init__.py | 23 +++++++ app/main/routes.py | 7 +++ app/models/__init__.py | 1 + app/models/schema.sql | 103 ++++++++++++++++++++++++++++++++ app/templates/cloud/index.html | 19 ++++++ requirements.txt | 3 + run.py | 6 ++ scripts/bootstrap_storage.sh | 13 ++++ scripts/create_tenant_layout.sh | 30 ++++++++++ 14 files changed, 409 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 PROJECT_STATE.md create mode 100644 README.md create mode 100644 VERSION create mode 100644 app/__init__.py create mode 100644 app/main/routes.py create mode 100644 app/models/__init__.py create mode 100644 app/models/schema.sql create mode 100644 app/templates/cloud/index.html create mode 100644 requirements.txt create mode 100644 run.py create mode 100755 scripts/bootstrap_storage.sh create mode 100755 scripts/create_tenant_layout.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fcdc621 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +FLASK_APP=run.py +FLASK_ENV=development +SECRET_KEY=change-me +MARIADB_HOST=127.0.0.1 +MARIADB_PORT=3306 +MARIADB_DB=otb_cloud +MARIADB_USER=otb_cloud +MARIADB_PASSWORD=change-me +STORAGE_ROOT=/tank/backups/otb-cloud diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..269eb7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ + +# Virtual environments +venv/ +.venv/ +env/ + +# Flask / runtime +instance/*.db +instance/*.sqlite +instance/*.sqlite3 +*.log +*.pid +tmp/ +uploads_tmp/ + +# Secrets / env +.env +.env.* +!.env.example + +# Build artifacts +dist/ +build/ +*.egg-info/ + +# Editors / OS +.vscode/ +.idea/ +*.swp +*~ +.DS_Store +Thumbs.db + +# Backups / scratch +*.bak +*.old +*.orig + +# Generated exports / tarballs +exports/ +tarballs/ + +# Android (future) +android/.gradle/ +android/local.properties +android/app/build/ + +# Node (future) +node_modules/ diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md new file mode 100644 index 0000000..6312c7b --- /dev/null +++ b/PROJECT_STATE.md @@ -0,0 +1,71 @@ +# PROJECT_STATE.md + +## Project +OTB Cloud + +## Current version +v0.1.0 + +## Build date +2026-04-12 + +## Host +vault3 + +## App path +/opt/otb_cloud + +## Purpose +Portal-authenticated secure backup and storage platform for customer files, including images, videos, documents, and other uploaded data. + +## Core requirements locked in +- Shared OTB branding, nav, footer, favicon +- Portal login / auth handoff through OTB Billing +- No unauthenticated file/account access +- MariaDB backend +- Vault3 storage root at `/tank/backups/otb-cloud` +- Tenant-isolated storage +- Device-defined source directories +- Immutable originals +- Derived-file processing workflow +- Search by filename and date +- Bulk zip export +- 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/ + +## Initial app modules planned +- auth +- main +- files +- jobs +- admin +- audit +- services +- models + +## Immediate next tasks +1. Add Flask app factory and blueprint registration +2. Add MariaDB config and SQL bootstrap schema +3. Add shared portal template integration +4. Add storage bootstrap script for vault3 +5. Add service card integration plan for OTB Billing portal + +## 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a99155b --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# OTB Cloud + +## v0.1.0 - 2026-04-12 +- Initial scaffold created on vault3 at /opt/otb_cloud +- MariaDB-backed architecture selected +- Modular Flask app structure created +- Device-based tenant storage model defined +- Shared OTB portal template architecture planned +- Core project documentation files added + +--- + +## Summary +OTB Cloud is a private portal-authenticated backup and storage platform for Outsidethebox.top. + +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.) +- 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 new file mode 100644 index 0000000..b82608c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.1.0 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..fe2d8a1 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,23 @@ +import os +from flask import Flask +from dotenv import load_dotenv + +def create_app(): + load_dotenv() + + app = Flask(__name__, instance_relative_config=True) + + app.config.from_mapping( + SECRET_KEY=os.getenv("SECRET_KEY", "change-me"), + MARIADB_HOST=os.getenv("MARIADB_HOST", "127.0.0.1"), + MARIADB_PORT=int(os.getenv("MARIADB_PORT", "3306")), + MARIADB_DB=os.getenv("MARIADB_DB", "otb_cloud"), + MARIADB_USER=os.getenv("MARIADB_USER", "otb_cloud"), + MARIADB_PASSWORD=os.getenv("MARIADB_PASSWORD", "change-me"), + STORAGE_ROOT=os.getenv("STORAGE_ROOT", "/tank/backups/otb-cloud"), + ) + + from .main.routes import bp as main_bp + app.register_blueprint(main_bp) + + return app diff --git a/app/main/routes.py b/app/main/routes.py new file mode 100644 index 0000000..ae99655 --- /dev/null +++ b/app/main/routes.py @@ -0,0 +1,7 @@ +from flask import Blueprint, render_template + +bp = Blueprint("main", __name__) + +@bp.route("/") +def index(): + return render_template("cloud/index.html") diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..0c0cd21 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# SQL schema is currently managed in app/models/schema.sql diff --git a/app/models/schema.sql b/app/models/schema.sql new file mode 100644 index 0000000..9cf0f38 --- /dev/null +++ b/app/models/schema.sql @@ -0,0 +1,103 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + portal_user_id INT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + display_name VARCHAR(255) NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_login_at DATETIME NULL +); + +CREATE TABLE IF NOT EXISTS tenants ( + id INT AUTO_INCREMENT PRIMARY KEY, + owner_user_id INT NOT NULL, + slug VARCHAR(100) NOT NULL UNIQUE, + storage_root VARCHAR(500) NOT NULL, + service_status VARCHAR(50) NOT NULL DEFAULT 'active', + retention_mode VARCHAR(50) NOT NULL DEFAULT 'standard', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_tenants_owner FOREIGN KEY (owner_user_id) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS devices ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + device_name VARCHAR(100) NOT NULL, + device_type VARCHAR(50) NOT NULL, + relative_path VARCHAR(255) NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_devices_tenant_name UNIQUE (tenant_id, device_name), + CONSTRAINT fk_devices_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) +); + +CREATE TABLE IF NOT EXISTS files ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + device_id INT NOT NULL, + parent_file_id BIGINT NULL, + file_kind VARCHAR(20) NOT NULL, + relative_path VARCHAR(1000) NOT NULL, + directory_path VARCHAR(1000) NOT NULL, + original_filename VARCHAR(255) NOT NULL, + basename VARCHAR(255) NOT NULL, + extension VARCHAR(50) NOT NULL, + mime_type VARCHAR(255) NULL, + size_bytes BIGINT NOT NULL DEFAULT 0, + sha256 CHAR(64) NULL, + capture_date DATETIME NULL, + uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_immutable TINYINT(1) NOT NULL DEFAULT 1, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + deleted_at DATETIME NULL, + CONSTRAINT fk_files_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id), + CONSTRAINT fk_files_device FOREIGN KEY (device_id) REFERENCES devices(id), + CONSTRAINT fk_files_parent FOREIGN KEY (parent_file_id) REFERENCES files(id) +); + +CREATE TABLE IF NOT EXISTS jobs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + file_id BIGINT NOT NULL, + job_type VARCHAR(100) NOT NULL, + options_json LONGTEXT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'queued', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + started_at DATETIME NULL, + completed_at DATETIME NULL, + output_file_id BIGINT NULL, + log_text LONGTEXT NULL, + CONSTRAINT fk_jobs_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id), + CONSTRAINT fk_jobs_file FOREIGN KEY (file_id) REFERENCES files(id) +); + +CREATE TABLE IF NOT EXISTS audit_logs ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NULL, + user_id INT NULL, + actor_type VARCHAR(20) NOT NULL, + event_type VARCHAR(100) NOT NULL, + file_id BIGINT NULL, + job_id BIGINT NULL, + ip_address VARCHAR(64) NULL, + user_agent VARCHAR(500) NULL, + event_detail LONGTEXT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_audit_tenant_created (tenant_id, created_at), + INDEX idx_audit_event_type (event_type) +); + +CREATE TABLE IF NOT EXISTS admin_access_tokens ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + issued_by_user_id INT NOT NULL, + used_by_admin_id INT NULL, + token_hash CHAR(64) NOT NULL, + purpose VARCHAR(255) NOT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'issued', + expires_at DATETIME NOT NULL, + used_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_admin_token_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id), + CONSTRAINT fk_admin_token_owner FOREIGN KEY (issued_by_user_id) REFERENCES users(id) +); diff --git a/app/templates/cloud/index.html b/app/templates/cloud/index.html new file mode 100644 index 0000000..dc08c77 --- /dev/null +++ b/app/templates/cloud/index.html @@ -0,0 +1,19 @@ +{% extends "portal_base.html" %} + +{% block title %}OTB Cloud{% endblock %} + +{% block content %} +
+
+
+

OTB Cloud

+

+ Private backup and storage platform for customer files, device uploads, and media processing. +

+

+ Initial scaffold is in place. Portal auth handoff, tenant isolation, searchable library, and processing jobs come next. +

+
+
+
+{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5b819a4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.1.0 +PyMySQL==1.1.1 +python-dotenv==1.0.1 diff --git a/run.py b/run.py new file mode 100644 index 0000000..953b7aa --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="127.0.0.1", port=5090, debug=True) diff --git a/scripts/bootstrap_storage.sh b/scripts/bootstrap_storage.sh new file mode 100755 index 0000000..e475fac --- /dev/null +++ b/scripts/bootstrap_storage.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +BASE="/tank/backups/otb-cloud" + +echo "===== ensuring base storage root exists =====" +sudo mkdir -p "$BASE/tenants" +sudo mkdir -p "$BASE/system/job-output" +sudo mkdir -p "$BASE/system/logs" +sudo mkdir -p "$BASE/system/quarantine" + +echo "===== base storage root prepared =====" +echo "===== script completed successfully =====" diff --git a/scripts/create_tenant_layout.sh b/scripts/create_tenant_layout.sh new file mode 100755 index 0000000..58bd228 --- /dev/null +++ b/scripts/create_tenant_layout.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e + +if [ $# -lt 2 ]; then + echo "usage: $0 [device_name ...]" + exit 1 +fi + +BASE="/tank/backups/otb-cloud/tenants" +TENANT="$1" +shift + +TENANT_ROOT="$BASE/$TENANT" + +echo "===== creating tenant root for $TENANT =====" +sudo mkdir -p "$TENANT_ROOT" + +for DEVICE in "$@"; do + sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/originals" + sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/derived" + sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/exports" + sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/deleted" + sudo mkdir -p "$TENANT_ROOT/devices/$DEVICE/tmp" +done + +sudo mkdir -p "$TENANT_ROOT/logs" +sudo mkdir -p "$TENANT_ROOT/support" + +echo "===== tenant layout created =====" +echo "===== script completed successfully ====="