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:
44
CHANGELOG.md
44
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 `<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)
|
||||
|
||||
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.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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/svg+xml" href="/assets/logo.svg">
|
||||
<meta name="csrf-token" content="">
|
||||
|
||||
@@ -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")} <small style="font-size:12px;color:gray;">${version}</small>`;
|
||||
|
||||
// Translate with fallback: if t(key) just echos the key, use a readable string.
|
||||
|
||||
@@ -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);
|
||||
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");
|
||||
localStorage.setItem("authBypass", String(!!config.loginOptions.authBypass));
|
||||
localStorage.setItem("authHeaderName", config.loginOptions.authHeaderName || "X-Remote-User");
|
||||
// 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";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
// Only run trash/restore for admins
|
||||
const isAdmin =
|
||||
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
|
||||
if (isAdmin) {
|
||||
setupTrashRestoreDelete();
|
||||
// NOTE: loadAdminConfigFunc() is called once in DOMContentLoaded; calling here would duplicate requests.
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -51,19 +51,10 @@ class AdminController
|
||||
* @return void Outputs a JSON response with configuration data.
|
||||
*/
|
||||
public function getConfig(): void
|
||||
{
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Load raw config (no disclosure yet)
|
||||
$config = AdminModel::getConfig();
|
||||
if (isset($config['error'])) {
|
||||
http_response_code(500);
|
||||
@@ -71,24 +62,44 @@ class AdminController
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build a safe subset for the front-end
|
||||
$safe = [
|
||||
'header_title' => $config['header_title'] ?? '',
|
||||
'loginOptions' => $config['loginOptions'] ?? [],
|
||||
// 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' => $config['enableWebDAV'] ?? false,
|
||||
'sharedMaxUploadSize' => $config['sharedMaxUploadSize'] ?? 0,
|
||||
'enableWebDAV' => (bool)($config['enableWebDAV'] ?? false),
|
||||
'sharedMaxUploadSize' => (int)($config['sharedMaxUploadSize'] ?? 0),
|
||||
|
||||
'oidc' => [
|
||||
'providerUrl' => $config['oidc']['providerUrl'] ?? '',
|
||||
'redirectUri' => $config['oidc']['redirectUri'] ?? '',
|
||||
// clientSecret and clientId never exposed here
|
||||
'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''),
|
||||
'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''),
|
||||
// never expose clientId / clientSecret
|
||||
],
|
||||
];
|
||||
|
||||
echo json_encode($safe);
|
||||
exit;
|
||||
$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",
|
||||
|
||||
Reference in New Issue
Block a user