From be605b45221997759582aaeb32a11d10dab546d0 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 19 Oct 2025 07:19:29 -0400 Subject: [PATCH] fix(config/ui): serve safe public config to non-admins; init early; gate trash UI to admins; dynamic title; demo toast (closes #56) --- CHANGELOG.md | 44 ++++++++++++++++ public/index.html | 2 +- public/js/adminPanel.js | 2 +- public/js/auth.js | 64 +++++++++++++++-------- public/js/main.js | 12 +++-- src/controllers/AdminController.php | 79 ++++++++++++++++------------- 6 files changed, 142 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0c2bc..fb8fbd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## Changes 10/19/2025 (v1.5.1) + +fix(config/ui): serve safe public config to non-admins; init early; gate trash UI to admins; dynamic title; demo toast (closes #56) + +Regular users were getting 403s from `/api/admin/getConfig.php`, breaking header title and login option rendering. Issue #56 tracks this. + +### What changed + +- **AdminController::getConfig** + - Return a **public, non-sensitive subset** of config for everyone (incl. unauthenticated and non-admin users): `header_title`, minimal `loginOptions` (disable* flags only), `globalOtpauthUrl`, `enableWebDAV`, `sharedMaxUploadSize`, and OIDC `providerUrl`/`redirectUri`. + - For **admins**, merge in admin-only fields (`authBypass`, `authHeaderName`). + - Never expose secrets or client IDs. +- **auth.js** + - `loadAdminConfigFunc()` now robustly handles empty/204 responses, writes sane defaults, and sets `document.title` from `header_title`. + - `showToast()` override: on `demo.filerise.net` shows a longer demo-creds toast; keeps TOTP “don’t nag” behavior. +- **main.js** + - Call `loadAdminConfigFunc()` early during app init. + - Run `setupTrashRestoreDelete()` **only for admins** (based on `localStorage.isAdmin`). +- **adminPanel.js** + - Bump visible version to **v1.5.1**. +- **index.html** + - Keep `FileRise` static; runtime title now driven by `loadAdminConfigFunc()`. + +### Security v1.5.1 + +- Prevents info disclosure by strictly limiting non-admin fields. +- Avoids noisy 403 for regular users while keeping admin-only data protected. + +### QA + +- As a non-admin: + - Opening the app no longer triggers a 403 on `getConfig.php`. + - Header title and login options render; document tab title updates to configured `header_title`. + - Trash/restore UI is not initialized. +- As an admin: + - Admin Panel loads extra fields; trash/restore UI initializes. + - Title updates correctly. +- On `demo.filerise.net`: + - Pre-login toast shows demo credentials for ~12s. + +Closes #56. + +--- + ## Changes 10/17/2025 (v1.5.0) Security and permission model overhaul. Tightens access controls with explicit, server‑side ACL checks across controllers and WebDAV. Introduces `read_own` for own‑only visibility and separates view from write so uploaders can’t automatically see others’ files. Fixes session warnings and aligns the admin UI with the new capabilities. diff --git a/public/index.html b/public/index.html index 5222fc4..b7c878b 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,7 @@ - FileRise + FileRise diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index 1396385..fd22c11 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -4,7 +4,7 @@ import { loadAdminConfigFunc } from './auth.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { sendRequest } from './networkUtils.js'; -const version = "v1.5.0"; +const version = "v1.5.1"; const adminTitle = `${t("admin_panel")} ${version}`; // Translate with fallback: if t(key) just echos the key, use a readable string. diff --git a/public/js/auth.js b/public/js/auth.js index cde8499..d085f31 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -36,13 +36,33 @@ window.currentOIDCConfig = currentOIDCConfig; window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1'; // override showToast to suppress the "Please log in to continue." toast during TOTP -function showToast(msgKey) { - const msg = t(msgKey); - if (window.pendingTOTP && msgKey === "please_log_in_to_continue") { + +function showToast(msgKeyOrText, type) { + const isDemoHost = window.location.hostname.toLowerCase() === "demo.filerise.net"; + + // If it's the pre-login prompt and we're on the demo site, show demo creds instead. + if (isDemoHost) { + return originalShowToast("Demo site — use: \nUsername: demo\nPassword: demo", 12000); + } + + // Don’t nag during pending TOTP, as you already had + if (window.pendingTOTP && msgKeyOrText === "please_log_in_to_continue") { return; } - originalShowToast(msg); + + // Translate if a key; otherwise pass through the raw text + let msg = msgKeyOrText; + try { + const translated = t(msgKeyOrText); + // If t() changed it or it's a key-like string, use the translation + if (typeof translated === "string" && translated !== msgKeyOrText) { + msg = translated; + } + } catch { /* if t() isn’t available here, just use the original */ } + + return originalShowToast(msg); } + window.showToast = showToast; const originalFetch = window.fetch; @@ -161,27 +181,31 @@ function updateLoginOptionsUIFromStorage() { export function loadAdminConfigFunc() { return fetch("/api/admin/getConfig.php", { credentials: "include" }) - .then(response => response.json()) - .then(config => { - localStorage.setItem("headerTitle", config.header_title || "FileRise"); + .then(async (response) => { + // If a proxy or some edge returns 204/empty, handle gracefully + let config = {}; + try { config = await response.json(); } catch { config = {}; } - // Update login options using the nested loginOptions object. - localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin); - localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth); - localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin); - localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise"); - localStorage.setItem("authBypass", String(!!config.loginOptions.authBypass)); - localStorage.setItem("authHeaderName", config.loginOptions.authHeaderName || "X-Remote-User"); + const headerTitle = config.header_title || "FileRise"; + localStorage.setItem("headerTitle", headerTitle); + + document.title = headerTitle; + const lo = config.loginOptions || {}; + localStorage.setItem("disableFormLogin", String(!!lo.disableFormLogin)); + localStorage.setItem("disableBasicAuth", String(!!lo.disableBasicAuth)); + localStorage.setItem("disableOIDCLogin", String(!!lo.disableOIDCLogin)); + localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise"); + // These may be absent for non-admins; default them + localStorage.setItem("authBypass", String(!!lo.authBypass)); + localStorage.setItem("authHeaderName", lo.authHeaderName || "X-Remote-User"); updateLoginOptionsUIFromStorage(); const headerTitleElem = document.querySelector(".header-title h1"); - if (headerTitleElem) { - headerTitleElem.textContent = config.header_title || "FileRise"; - } + if (headerTitleElem) headerTitleElem.textContent = headerTitle; }) .catch(() => { - // Use defaults. + // Fallback defaults if request truly fails localStorage.setItem("headerTitle", "FileRise"); localStorage.setItem("disableFormLogin", "false"); localStorage.setItem("disableBasicAuth", "false"); @@ -190,9 +214,7 @@ export function loadAdminConfigFunc() { updateLoginOptionsUIFromStorage(); const headerTitleElem = document.querySelector(".header-title h1"); - if (headerTitleElem) { - headerTitleElem.textContent = "FileRise"; - } + if (headerTitleElem) headerTitleElem.textContent = "FileRise"; }); } diff --git a/public/js/main.js b/public/js/main.js index 0f1024b..d4721bb 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -108,7 +108,7 @@ export function initializeApp() { window.currentFolder = "root"; const stored = localStorage.getItem('showFoldersInList'); window.showFoldersInList = stored === null ? true : stored === 'true'; - + loadAdminConfigFunc(); initTagSearch(); loadFileList(window.currentFolder); @@ -139,8 +139,12 @@ export function initializeApp() { initFileActions(); initUpload(); loadFolderTree(); - setupTrashRestoreDelete(); - // NOTE: loadAdminConfigFunc() is called once in DOMContentLoaded; calling here would duplicate requests. + // Only run trash/restore for admins + const isAdmin = + localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true'; + if (isAdmin) { + setupTrashRestoreDelete(); + } const helpBtn = document.getElementById("folderHelpBtn"); const helpTooltip = document.getElementById("folderHelpTooltip"); @@ -216,7 +220,7 @@ window.openDownloadModal = openDownloadModal; window.currentFolder = "root"; document.addEventListener("DOMContentLoaded", function () { - // Load admin config once here; non-admins may get 403, which is fine. + // Load admin config early loadAdminConfigFunc(); // i18n diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 759afbd..3d283db 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -51,44 +51,55 @@ class AdminController * @return void Outputs a JSON response with configuration data. */ public function getConfig(): void - { - header('Content-Type: application/json'); +{ + header('Content-Type: application/json'); - // Require authenticated admin to read config (prevents information disclosure) - if ( - empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || - empty($_SESSION['isAdmin']) - ) { - http_response_code(403); - echo json_encode(['error' => 'Unauthorized access.']); - exit; - } - - $config = AdminModel::getConfig(); - if (isset($config['error'])) { - http_response_code(500); - echo json_encode(['error' => $config['error']]); - exit; - } - - // Build a safe subset for the front-end - $safe = [ - 'header_title' => $config['header_title'] ?? '', - 'loginOptions' => $config['loginOptions'] ?? [], - 'globalOtpauthUrl' => $config['globalOtpauthUrl'] ?? '', - 'enableWebDAV' => $config['enableWebDAV'] ?? false, - 'sharedMaxUploadSize' => $config['sharedMaxUploadSize'] ?? 0, - 'oidc' => [ - 'providerUrl' => $config['oidc']['providerUrl'] ?? '', - 'redirectUri' => $config['oidc']['redirectUri'] ?? '', - // clientSecret and clientId never exposed here - ], - ]; - - echo json_encode($safe); + // Load raw config (no disclosure yet) + $config = AdminModel::getConfig(); + if (isset($config['error'])) { + http_response_code(500); + echo json_encode(['error' => $config['error']]); exit; } + // Minimal, safe subset for all callers (unauth users and regular users) + $public = [ + 'header_title' => $config['header_title'] ?? 'FileRise', + 'loginOptions' => [ + // expose only what the login page / header needs + 'disableFormLogin' => (bool)($config['loginOptions']['disableFormLogin'] ?? false), + 'disableBasicAuth' => (bool)($config['loginOptions']['disableBasicAuth'] ?? false), + 'disableOIDCLogin' => (bool)($config['loginOptions']['disableOIDCLogin'] ?? false), + ], + 'globalOtpauthUrl' => $config['globalOtpauthUrl'] ?? '', + 'enableWebDAV' => (bool)($config['enableWebDAV'] ?? false), + 'sharedMaxUploadSize' => (int)($config['sharedMaxUploadSize'] ?? 0), + + 'oidc' => [ + 'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''), + 'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''), + // never expose clientId / clientSecret + ], + ]; + + $isAdmin = !empty($_SESSION['authenticated']) && !empty($_SESSION['isAdmin']); + + if ($isAdmin) { + // Add admin-only fields (used by Admin Panel UI) + $adminExtra = [ + 'loginOptions' => array_merge($public['loginOptions'], [ + 'authBypass' => (bool)($config['loginOptions']['authBypass'] ?? false), + 'authHeaderName' => (string)($config['loginOptions']['authHeaderName'] ?? 'X-Remote-User'), + ]), + ]; + echo json_encode(array_merge($public, $adminExtra)); + return; + } + + // Non-admins / unauthenticated: only the public subset + echo json_encode($public); +} + /** * @OA\Put( * path="/api/admin/updateConfig.php",