fix(config/ui): serve safe public config to non-admins; init early; gate trash UI to admins; dynamic title; demo toast (closes #56)

This commit is contained in:
Ryan
2025-10-19 07:19:29 -04:00
committed by GitHub
parent 090286164d
commit be605b4522
6 changed files with 142 additions and 61 deletions

View File

@@ -1,5 +1,49 @@
# Changelog # 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 “dont 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 `<title>FileRise</title>` 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) ## Changes 10/17/2025 (v1.5.0)
Security and permission model overhaul. Tightens access controls with explicit, serverside ACL checks across controllers and WebDAV. Introduces `read_own` for ownonly visibility and separates view from write so uploaders cant automatically see others files. Fixes session warnings and aligns the admin UI with the new capabilities. Security and permission model overhaul. Tightens access controls with explicit, serverside ACL checks across controllers and WebDAV. Introduces `read_own` for ownonly visibility and separates view from write so uploaders cant automatically see others files. Fixes session warnings and aligns the admin UI with the new capabilities.

View File

@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title data-i18n-key="title">FileRise</title> <title>FileRise</title>
<link rel="icon" type="image/png" href="/assets/logo.png"> <link rel="icon" type="image/png" href="/assets/logo.png">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg"> <link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<meta name="csrf-token" content=""> <meta name="csrf-token" content="">

View File

@@ -4,7 +4,7 @@ import { loadAdminConfigFunc } from './auth.js';
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
const version = "v1.5.0"; const version = "v1.5.1";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`; const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
// Translate with fallback: if t(key) just echos the key, use a readable string. // Translate with fallback: if t(key) just echos the key, use a readable string.

View File

@@ -36,13 +36,33 @@ window.currentOIDCConfig = currentOIDCConfig;
window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1'; window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1';
// override showToast to suppress the "Please log in to continue." toast during TOTP // override showToast to suppress the "Please log in to continue." toast during TOTP
function showToast(msgKey) {
const msg = t(msgKey); function showToast(msgKeyOrText, type) {
if (window.pendingTOTP && msgKey === "please_log_in_to_continue") { 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);
}
// Dont nag during pending TOTP, as you already had
if (window.pendingTOTP && msgKeyOrText === "please_log_in_to_continue") {
return; 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() isnt available here, just use the original */ }
return originalShowToast(msg);
} }
window.showToast = showToast; window.showToast = showToast;
const originalFetch = window.fetch; const originalFetch = window.fetch;
@@ -161,27 +181,31 @@ function updateLoginOptionsUIFromStorage() {
export function loadAdminConfigFunc() { export function loadAdminConfigFunc() {
return fetch("/api/admin/getConfig.php", { credentials: "include" }) return fetch("/api/admin/getConfig.php", { credentials: "include" })
.then(response => response.json()) .then(async (response) => {
.then(config => { // If a proxy or some edge returns 204/empty, handle gracefully
localStorage.setItem("headerTitle", config.header_title || "FileRise"); let config = {};
try { config = await response.json(); } catch { config = {}; }
// Update login options using the nested loginOptions object. const headerTitle = config.header_title || "FileRise";
localStorage.setItem("disableFormLogin", config.loginOptions.disableFormLogin); localStorage.setItem("headerTitle", headerTitle);
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin); document.title = headerTitle;
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise"); const lo = config.loginOptions || {};
localStorage.setItem("authBypass", String(!!config.loginOptions.authBypass)); localStorage.setItem("disableFormLogin", String(!!lo.disableFormLogin));
localStorage.setItem("authHeaderName", config.loginOptions.authHeaderName || "X-Remote-User"); 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(); updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1"); const headerTitleElem = document.querySelector(".header-title h1");
if (headerTitleElem) { if (headerTitleElem) headerTitleElem.textContent = headerTitle;
headerTitleElem.textContent = config.header_title || "FileRise";
}
}) })
.catch(() => { .catch(() => {
// Use defaults. // Fallback defaults if request truly fails
localStorage.setItem("headerTitle", "FileRise"); localStorage.setItem("headerTitle", "FileRise");
localStorage.setItem("disableFormLogin", "false"); localStorage.setItem("disableFormLogin", "false");
localStorage.setItem("disableBasicAuth", "false"); localStorage.setItem("disableBasicAuth", "false");
@@ -190,9 +214,7 @@ export function loadAdminConfigFunc() {
updateLoginOptionsUIFromStorage(); updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1"); const headerTitleElem = document.querySelector(".header-title h1");
if (headerTitleElem) { if (headerTitleElem) headerTitleElem.textContent = "FileRise";
headerTitleElem.textContent = "FileRise";
}
}); });
} }

View File

@@ -108,7 +108,7 @@ export function initializeApp() {
window.currentFolder = "root"; window.currentFolder = "root";
const stored = localStorage.getItem('showFoldersInList'); const stored = localStorage.getItem('showFoldersInList');
window.showFoldersInList = stored === null ? true : stored === 'true'; window.showFoldersInList = stored === null ? true : stored === 'true';
loadAdminConfigFunc();
initTagSearch(); initTagSearch();
loadFileList(window.currentFolder); loadFileList(window.currentFolder);
@@ -139,8 +139,12 @@ export function initializeApp() {
initFileActions(); initFileActions();
initUpload(); initUpload();
loadFolderTree(); loadFolderTree();
setupTrashRestoreDelete(); // Only run trash/restore for admins
// NOTE: loadAdminConfigFunc() is called once in DOMContentLoaded; calling here would duplicate requests. const isAdmin =
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
if (isAdmin) {
setupTrashRestoreDelete();
}
const helpBtn = document.getElementById("folderHelpBtn"); const helpBtn = document.getElementById("folderHelpBtn");
const helpTooltip = document.getElementById("folderHelpTooltip"); const helpTooltip = document.getElementById("folderHelpTooltip");
@@ -216,7 +220,7 @@ window.openDownloadModal = openDownloadModal;
window.currentFolder = "root"; window.currentFolder = "root";
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Load admin config once here; non-admins may get 403, which is fine. // Load admin config early
loadAdminConfigFunc(); loadAdminConfigFunc();
// i18n // i18n

View File

@@ -51,44 +51,55 @@ class AdminController
* @return void Outputs a JSON response with configuration data. * @return void Outputs a JSON response with configuration data.
*/ */
public function getConfig(): void public function getConfig(): void
{ {
header('Content-Type: application/json'); header('Content-Type: application/json');
// Require authenticated admin to read config (prevents information disclosure) // Load raw config (no disclosure yet)
if ( $config = AdminModel::getConfig();
empty($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true || if (isset($config['error'])) {
empty($_SESSION['isAdmin']) http_response_code(500);
) { echo json_encode(['error' => $config['error']]);
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);
exit; 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( * @OA\Put(
* path="/api/admin/updateConfig.php", * path="/api/admin/updateConfig.php",