Implement debug API for portal files and enhance file listing functionality: add a no-auth endpoint for troubleshooting, improve error handling, and streamline the portal files listing logic. Update HTML template to handle session expiration gracefully and provide a read-only fallback for unauthenticated users.
This commit is contained in:
@@ -424,6 +424,27 @@ def serve_portal_file(filename):
|
|||||||
return send_file(path, as_attachment=False, download_name=filename.split("/")[-1])
|
return send_file(path, as_attachment=False, download_name=filename.split("/")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/portal-files-debug")
|
||||||
|
def api_portal_files_debug():
|
||||||
|
"""No-auth debug: what PORTAL_FILES_DIR the process sees (for troubleshooting)."""
|
||||||
|
try:
|
||||||
|
names = []
|
||||||
|
if PORTAL_FILES_DIR.is_dir():
|
||||||
|
for p in sorted(PORTAL_FILES_DIR.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
||||||
|
if ".." in p.name or p.name.startswith("."):
|
||||||
|
continue
|
||||||
|
names.append({"name": p.name, "type": "dir" if p.is_dir() else "file"})
|
||||||
|
return jsonify({
|
||||||
|
"portal_files_dir": str(PORTAL_FILES_DIR),
|
||||||
|
"exists": PORTAL_FILES_DIR.exists(),
|
||||||
|
"is_dir": PORTAL_FILES_DIR.is_dir(),
|
||||||
|
"items": names,
|
||||||
|
"CM4_PROVISIONING_DIR": os.environ.get("CM4_PROVISIONING_DIR"),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/status")
|
@app.route("/api/status")
|
||||||
def api_status():
|
def api_status():
|
||||||
return jsonify(read_status())
|
return jsonify(read_status())
|
||||||
@@ -610,29 +631,26 @@ def _save_portal_descriptions(descriptions):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/portal-files")
|
def _portal_files_list_impl(subpath):
|
||||||
@require_admin
|
"""Shared impl for listing portal files. Returns (items, descriptions, base_url, current_path)."""
|
||||||
def api_portal_files_list():
|
|
||||||
"""List one level: root or contents of path=... (folders and files)."""
|
|
||||||
subpath = request.args.get("path", "").strip().strip("/")
|
|
||||||
base_url = request.host_url.rstrip("/") + "/files/"
|
base_url = request.host_url.rstrip("/") + "/files/"
|
||||||
empty = {"items": [], "base_url": base_url, "descriptions": {}, "current_path": subpath, "portal_files_dir": str(PORTAL_FILES_DIR)}
|
empty_items = []
|
||||||
if ".." in subpath or "\\" in subpath:
|
if ".." in subpath or "\\" in subpath:
|
||||||
return jsonify(empty)
|
return empty_items, {}, base_url, subpath
|
||||||
if not PORTAL_FILES_DIR.is_dir():
|
if not PORTAL_FILES_DIR.is_dir():
|
||||||
try:
|
try:
|
||||||
PORTAL_FILES_DIR.mkdir(parents=True, exist_ok=True)
|
PORTAL_FILES_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
if not PORTAL_FILES_DIR.is_dir():
|
if not PORTAL_FILES_DIR.is_dir():
|
||||||
return jsonify(empty)
|
return empty_items, {}, base_url, subpath
|
||||||
list_dir = (PORTAL_FILES_DIR / subpath).resolve() if subpath else PORTAL_FILES_DIR
|
list_dir = (PORTAL_FILES_DIR / subpath).resolve() if subpath else PORTAL_FILES_DIR
|
||||||
try:
|
try:
|
||||||
list_dir.relative_to(PORTAL_FILES_DIR.resolve())
|
list_dir.relative_to(PORTAL_FILES_DIR.resolve())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return jsonify({**empty, "current_path": subpath})
|
return empty_items, {}, base_url, subpath
|
||||||
if not list_dir.is_dir():
|
if not list_dir.is_dir():
|
||||||
return jsonify({**empty, "current_path": subpath})
|
return empty_items, {}, base_url, subpath
|
||||||
items = []
|
items = []
|
||||||
for p in sorted(list_dir.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
for p in sorted(list_dir.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
||||||
if ".." in p.name or p.name.startswith("."):
|
if ".." in p.name or p.name.startswith("."):
|
||||||
@@ -646,7 +664,28 @@ def api_portal_files_list():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
descriptions = _load_portal_descriptions()
|
descriptions = _load_portal_descriptions()
|
||||||
return jsonify({"items": items, "base_url": base_url, "descriptions": descriptions, "current_path": subpath, "portal_files_dir": str(PORTAL_FILES_DIR)})
|
return items, descriptions, base_url, subpath
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/portal-files")
|
||||||
|
def api_portal_files_list():
|
||||||
|
"""List one level: root or contents of path=... (folders and files). ?debug=1 allows unauthenticated read-only list."""
|
||||||
|
subpath = request.args.get("path", "").strip().strip("/")
|
||||||
|
debug = request.args.get("debug") == "1"
|
||||||
|
if not debug and not session.get("admin_logged_in"):
|
||||||
|
if request.is_json or request.path.startswith("/api/"):
|
||||||
|
return jsonify({"ok": False, "error": "Login required"}), 401
|
||||||
|
return redirect(url_for("login", next=request.url))
|
||||||
|
items, descriptions, base_url, subpath = _portal_files_list_impl(subpath)
|
||||||
|
resp = jsonify({
|
||||||
|
"items": items,
|
||||||
|
"base_url": base_url,
|
||||||
|
"descriptions": descriptions,
|
||||||
|
"current_path": subpath,
|
||||||
|
"portal_files_dir": str(PORTAL_FILES_DIR),
|
||||||
|
})
|
||||||
|
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/portal-files/descriptions", methods=["GET", "PATCH"])
|
@app.route("/api/portal-files/descriptions", methods=["GET", "PATCH"])
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function authFetch(url, opts) {
|
function authFetch(url, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
opts.credentials = opts.credentials || 'same-origin';
|
||||||
return fetch(url, opts).then(function(r) {
|
return fetch(url, opts).then(function(r) {
|
||||||
if (r.status === 401) { window.location = '/login?next=' + encodeURIComponent(window.location.pathname); return Promise.reject(new Error('Login required')); }
|
if (r.status === 401) { window.location = '/login?next=' + encodeURIComponent(window.location.pathname); return Promise.reject(new Error('Login required')); }
|
||||||
return r;
|
return r;
|
||||||
@@ -167,8 +168,23 @@
|
|||||||
|
|
||||||
function fetchPortal() {
|
function fetchPortal() {
|
||||||
var url = '/api/portal-files';
|
var url = '/api/portal-files';
|
||||||
if (currentPath) url += '?path=' + encodeURIComponent(currentPath);
|
if (currentPath) url += (url.indexOf('?') >= 0 ? '&' : '?') + 'path=' + encodeURIComponent(currentPath);
|
||||||
authFetch(url).then(function(r) { return r.json(); }).then(renderPortal).catch(function() {});
|
authFetch(url).then(function(r) { return r.json(); }).then(renderPortal).catch(function(err) {
|
||||||
|
document.getElementById('portalEmpty').style.display = 'block';
|
||||||
|
document.getElementById('portalEmpty').textContent = 'Could not load list (session may have expired). Trying read-only list…';
|
||||||
|
var fallbackUrl = '/api/portal-files?debug=1' + (currentPath ? '&path=' + encodeURIComponent(currentPath) : '');
|
||||||
|
fetch(fallbackUrl).then(function(r) { return r.json(); }).then(function(data) {
|
||||||
|
if (data.items && data.items.length) {
|
||||||
|
document.getElementById('portalEmpty').style.display = 'none';
|
||||||
|
renderPortal(data);
|
||||||
|
document.getElementById('portalDir').textContent = (data.portal_files_dir || '—') + ' (read-only; log in to edit)';
|
||||||
|
} else {
|
||||||
|
document.getElementById('portalEmpty').textContent = 'Server sees ' + (data.items ? data.items.length : 0) + ' item(s) at ' + (data.portal_files_dir || '?') + '. Log in to see and edit.';
|
||||||
|
}
|
||||||
|
}).catch(function() {
|
||||||
|
document.getElementById('portalEmpty').textContent = 'Could not load list. Log out and log in again, then refresh.';
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('newFolderBtn').onclick = function() {
|
document.getElementById('newFolderBtn').onclick = function() {
|
||||||
|
|||||||
Reference in New Issue
Block a user