You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
385 lines
12 KiB
385 lines
12 KiB
from functools import wraps |
|
from pathlib import Path |
|
from datetime import datetime, timezone |
|
from werkzeug.utils import secure_filename |
|
|
|
from flask import Blueprint, flash, redirect, render_template, request, session, url_for, current_app |
|
|
|
from app.db import get_db |
|
from app.auth.utils import create_device_directories, remove_device_directories, slugify_device_name, compute_sha256 |
|
|
|
bp = Blueprint("main", __name__) |
|
|
|
def portal_session_required(view_func): |
|
@wraps(view_func) |
|
def wrapped(*args, **kwargs): |
|
if "otb_user_id" not in session or "otb_tenant_id" not in session: |
|
return redirect(url_for("auth.login_required_notice")) |
|
return view_func(*args, **kwargs) |
|
return wrapped |
|
|
|
def _client_ip(): |
|
return request.headers.get("X-Forwarded-For", request.remote_addr) |
|
|
|
def _tenant_root() -> Path: |
|
return Path(current_app.config["STORAGE_ROOT"]) / "tenants" / session["otb_tenant_slug"] |
|
|
|
def _stored_name(original_name: str) -> str: |
|
safe = secure_filename(original_name or "upload.bin") |
|
if not safe: |
|
safe = "upload.bin" |
|
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") |
|
return f"{ts}__{safe}" |
|
|
|
@bp.route("/") |
|
def index(): |
|
if "otb_user_id" in session: |
|
return redirect(url_for("main.dashboard")) |
|
return redirect(url_for("auth.login_required_notice")) |
|
|
|
@bp.route("/dashboard") |
|
@portal_session_required |
|
def dashboard(): |
|
db = get_db() |
|
|
|
with db.cursor() as cur: |
|
cur.execute( |
|
""" |
|
SELECT id, device_name, device_type, relative_path, is_active, created_at |
|
FROM devices |
|
WHERE tenant_id = %s |
|
ORDER BY id |
|
""", |
|
(session["otb_tenant_id"],), |
|
) |
|
devices = cur.fetchall() |
|
|
|
return render_template( |
|
"cloud/dashboard.html", |
|
user_email=session.get("otb_email"), |
|
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"], |
|
_client_ip(), |
|
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")) |
|
|
|
@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"], |
|
_client_ip(), |
|
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")) |
|
|
|
@bp.route("/devices/<int:device_id>/upload", methods=["GET", "POST"]) |
|
@portal_session_required |
|
def upload_files(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")) |
|
|
|
if request.method == "GET": |
|
return render_template( |
|
"cloud/upload.html", |
|
user_email=session.get("otb_email"), |
|
tenant_slug=session.get("otb_tenant_slug"), |
|
device=device, |
|
) |
|
|
|
files = request.files.getlist("files") |
|
files = [f for f in files if f and f.filename] |
|
|
|
if not files: |
|
flash("Please choose at least one file to upload.", "warning") |
|
return render_template( |
|
"cloud/upload.html", |
|
user_email=session.get("otb_email"), |
|
tenant_slug=session.get("otb_tenant_slug"), |
|
device=device, |
|
) |
|
|
|
upload_base = _tenant_root() / device["relative_path"] / "originals" |
|
upload_base.mkdir(parents=True, exist_ok=True) |
|
|
|
uploaded_count = 0 |
|
|
|
with db.cursor() as cur: |
|
for incoming in files: |
|
original_filename = incoming.filename or "upload.bin" |
|
stored_name = _stored_name(original_filename) |
|
target_path = upload_base / stored_name |
|
|
|
incoming.save(target_path) |
|
|
|
size_bytes = target_path.stat().st_size |
|
sha256 = compute_sha256(target_path) |
|
|
|
base_name = original_filename.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] |
|
if "." in base_name: |
|
basename, extension = base_name.rsplit(".", 1) |
|
else: |
|
basename, extension = base_name, "" |
|
|
|
relative_path = f"{device['relative_path']}/originals/{stored_name}" |
|
directory_path = f"{device['relative_path']}/originals" |
|
|
|
cur.execute( |
|
""" |
|
INSERT INTO files ( |
|
tenant_id, device_id, parent_file_id, file_kind, relative_path, directory_path, |
|
original_filename, basename, extension, mime_type, size_bytes, sha256, |
|
capture_date, uploaded_at, is_immutable, is_deleted, deleted_at |
|
) VALUES ( |
|
%s, %s, NULL, 'original', %s, %s, |
|
%s, %s, %s, %s, %s, %s, |
|
NULL, UTC_TIMESTAMP(), 1, 0, NULL |
|
) |
|
""", |
|
( |
|
session["otb_tenant_id"], |
|
device["id"], |
|
relative_path, |
|
directory_path, |
|
original_filename, |
|
basename, |
|
extension, |
|
incoming.mimetype or None, |
|
size_bytes, |
|
sha256, |
|
), |
|
) |
|
|
|
file_id = cur.lastrowid |
|
|
|
cur.execute( |
|
""" |
|
INSERT INTO audit_logs ( |
|
tenant_id, user_id, actor_type, event_type, file_id, ip_address, user_agent, event_detail |
|
) VALUES (%s, %s, 'user', 'file_uploaded', %s, %s, %s, %s) |
|
""", |
|
( |
|
session["otb_tenant_id"], |
|
session["otb_user_id"], |
|
file_id, |
|
_client_ip(), |
|
request.headers.get("User-Agent", ""), |
|
f"Uploaded '{original_filename}' to {relative_path}", |
|
), |
|
) |
|
|
|
uploaded_count += 1 |
|
|
|
db.commit() |
|
|
|
flash(f"Uploaded {uploaded_count} file(s) to {device['device_name']}.", "success") |
|
return redirect(url_for("main.dashboard")) |
|
|
|
@bp.route("/devices/<int:device_id>/files", methods=["GET"]) |
|
@portal_session_required |
|
def browse_device_files(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 |
|
id, |
|
file_kind, |
|
relative_path, |
|
directory_path, |
|
original_filename, |
|
basename, |
|
extension, |
|
mime_type, |
|
size_bytes, |
|
sha256, |
|
uploaded_at, |
|
is_immutable |
|
FROM files |
|
WHERE tenant_id = %s |
|
AND device_id = %s |
|
AND is_deleted = 0 |
|
ORDER BY uploaded_at DESC, id DESC |
|
""", |
|
(session["otb_tenant_id"], device_id), |
|
) |
|
files = cur.fetchall() |
|
|
|
return render_template( |
|
"cloud/device_files.html", |
|
user_email=session.get("otb_email"), |
|
tenant_slug=session.get("otb_tenant_slug"), |
|
device=device, |
|
files=files, |
|
file_count=len(files), |
|
)
|
|
|