release(v1.7.5): CSP hardening, API-backed previews, flicker-free theming, cache tuning & deploy script (closes #50)

This commit is contained in:
Ryan
2025-11-02 00:32:03 -04:00
committed by GitHub
parent e509b7ac9c
commit b7d7f7c3ce
11 changed files with 699 additions and 336 deletions

View File

@@ -67,32 +67,25 @@ function isDemoHost() {
}
function showLoginTip(message) {
const form = document.getElementById('loginForm');
if (!form) return;
let tip = document.getElementById('fr-login-tip');
if (!tip) {
tip = document.createElement('div');
tip.id = 'fr-login-tip';
tip.className = 'alert alert-info'; // fine even without Bootstrap
tip.style.marginTop = '8px';
form.prepend(tip);
}
// Clear & rebuild so we can add the demo hint cleanly
tip.textContent = '';
tip.append(document.createTextNode(message || ''));
if (isDemoHost()) {
const line = document.createElement('div');
line.style.marginTop = '6px';
const mk = (txt) => { const k = document.createElement('code'); k.textContent = txt; return k; };
line.append(
document.createTextNode('Demo login — user: '), mk('demo'),
document.createTextNode(' · pass: '), mk('demo')
);
const tip = document.getElementById('fr-login-tip');
if (!tip) return;
tip.innerHTML = ''; // clear
if (message) tip.append(document.createTextNode(message));
if (location.hostname.replace(/^www\./, '') === 'demo.filerise.net') {
const line = document.createElement('div'); line.style.marginTop = '6px';
const mk = t => { const k = document.createElement('code'); k.textContent = t; return k; };
line.append(document.createTextNode('Demo login — user: '), mk('demo'),
document.createTextNode(' · pass: '), mk('demo'));
tip.append(line);
}
tip.style.display = 'block'; // reveal without shifting layout
}
async function hideOverlaySmoothly(overlay) {
if (!overlay) return;
try { await document.fonts?.ready; } catch { }
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
overlay.style.display = 'none';
}
function wireModalEnterDefault() {
@@ -322,7 +315,6 @@ function applyDarkMode({ fromSystemChange = false } = {}) {
let stored = null;
try { stored = localStorage.getItem('darkMode'); } catch { }
// If no stored pref, fall back to system
let isDark = (stored === null)
? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
: (stored === '1' || stored === 'true');
@@ -336,15 +328,26 @@ function applyDarkMode({ fromSystemChange = false } = {}) {
el.setAttribute('data-theme', isDark ? 'dark' : 'light');
});
// keep UA chrome & bg consistent post-toggle
const bg = isDark ? '#121212' : '#ffffff';
root.style.backgroundColor = bg;
root.style.colorScheme = isDark ? 'dark' : 'light';
if (body) {
body.style.backgroundColor = bg;
body.style.colorScheme = isDark ? 'dark' : 'light';
}
const mt = document.querySelector('meta[name="theme-color"]');
if (mt) mt.content = bg;
const mcs = document.querySelector('meta[name="color-scheme"]');
if (mcs) mcs.content = isDark ? 'dark light' : 'light dark';
const btn = document.getElementById('darkModeToggle');
const icon = document.getElementById('darkModeIcon');
if (icon) icon.textContent = isDark ? 'light_mode' : 'dark_mode';
if (btn) {
const ttOn = (typeof t === 'function' ? t('switch_to_dark_mode') : 'Switch to dark mode');
const ttOff = (typeof t === 'function' ? t('switch_to_light_mode') : 'Switch to light mode');
const aria = (typeof t === 'function' ? (isDark ? t('light_mode') : t('dark_mode')) : (isDark ? 'Light mode' : 'Dark mode'));
btn.classList.toggle('active', isDark);
btn.setAttribute('aria-label', aria);
btn.setAttribute('title', isDark ? ttOff : ttOn);
@@ -381,6 +384,9 @@ function bindDarkMode() {
// ---------- tiny utils ----------
const $ = (s, root = document) => root.querySelector(s);
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
// Safe show/hide that work with both CSS and [hidden]
const unhide = (el) => { if (!el) return; el.removeAttribute('hidden'); el.style.display = ''; };
const hideEl = (el) => { if (!el) return; el.setAttribute('hidden', ''); el.style.display = 'none'; };
const show = (el) => {
if (!el) return;
el.hidden = false; el.classList?.remove('d-none', 'hidden');
@@ -394,28 +400,88 @@ function bindDarkMode() {
};
// ---------- site config / auth ----------
function applySiteConfig(cfg) {
function applySiteConfig(cfg, { phase = 'final' } = {}) {
try {
const title = (cfg && cfg.header_title) ? String(cfg.header_title) : 'FileRise';
// Always keep <title> correct early (no visual flicker)
document.title = title;
const h1 = document.querySelector('.header-title h1'); if (h1) h1.textContent = title;
// --- Login options (apply in BOTH phases so login page is correct) ---
const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {};
const disableForm = !!lo.disableFormLogin;
const disableOIDC = !!lo.disableOIDCLogin;
const disableForm = !!lo.disableFormLogin;
const disableOIDC = !!lo.disableOIDCLogin;
const disableBasic = !!lo.disableBasicAuth;
const row = $('#loginForm'); if (row) row.style.display = disableForm ? 'none' : '';
const oidc = $('#oidcLoginBtn'); if (oidc) oidc.style.display = disableOIDC ? 'none' : '';
const row = $('#loginForm');
if (row) {
if (disableForm) {
row.setAttribute('hidden', '');
row.style.display = ''; // don't leave display:none lying around
} else {
row.removeAttribute('hidden');
row.style.display = '';
}
}
const oidc = $('#oidcLoginBtn'); if (oidc) oidc.style.display = disableOIDC ? 'none' : '';
const basic = document.querySelector('a[href="/api/auth/login_basic.php"]');
if (basic) basic.style.display = disableBasic ? 'none' : '';
// --- Header <h1> only in the FINAL phase (prevents visible flips) ---
if (phase === 'final') {
const h1 = document.querySelector('.header-title h1');
if (h1) {
// prevent i18n or legacy from overwriting it
if (h1.hasAttribute('data-i18n-key')) h1.removeAttribute('data-i18n-key');
if (h1.textContent !== title) h1.textContent = title;
// lock it so late code can't stomp it
if (!h1.__titleLock) {
const mo = new MutationObserver(() => {
if (h1.textContent !== title) h1.textContent = title;
});
mo.observe(h1, { childList: true, characterData: true, subtree: true });
h1.__titleLock = mo;
}
}
}
} catch { }
}
async function readyToReveal() {
// Wait for CSS + fonts so the first revealed frame is fully styled
try { await (window.__CSS_PROMISE__ || Promise.resolve()); } catch { }
try { await document.fonts?.ready; } catch { }
// Give layout one paint to settle
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
}
async function revealAppAndHideOverlay() {
const appRoot = document.getElementById('appRoot');
const overlay = document.getElementById('loadingOverlay');
await readyToReveal();
if (appRoot) appRoot.style.visibility = 'visible';
if (overlay) {
overlay.style.transition = 'opacity .18s ease-out';
overlay.style.opacity = '0';
setTimeout(() => { overlay.style.display = 'none'; }, 220);
}
}
async function loadSiteConfig() {
try {
const r = await fetch('/api/siteConfig.php', { credentials: 'include' });
const j = await r.json().catch(() => ({})); applySiteConfig(j);
} catch { applySiteConfig({}); }
const j = await r.json().catch(() => ({}));
window.__FR_SITE_CFG__ = j || {};
// Early pass: title + login options (skip touching <h1> to avoid flicker)
applySiteConfig(window.__FR_SITE_CFG__, { phase: 'early' });
return window.__FR_SITE_CFG__;
} catch {
window.__FR_SITE_CFG__ = {};
applySiteConfig({}, { phase: 'early' });
return null;
}
}
async function primeCsrf() {
try {
@@ -665,7 +731,6 @@ function bindDarkMode() {
function forceLoginVisible() {
show($('#main'));
show($('#loginForm'));
hide($('.main-wrapper'));
const hb = $('.header-buttons'); if (hb) hb.style.visibility = 'hidden';
const ov = $('#loadingOverlay'); if (ov) ov.style.display = 'none';
}
@@ -809,8 +874,7 @@ function bindDarkMode() {
window.__FR_FLAGS.booted = true;
ensureToastReady();
// show chrome
const wrap = document.querySelector('.main-wrapper'); if (wrap) { wrap.hidden = false; wrap.classList?.remove('d-none', 'hidden'); wrap.style.display = 'block'; }
const lf = document.getElementById('loginForm'); if (lf) lf.style.display = 'none';
const hb = document.querySelector('.header-buttons'); if (hb) hb.style.visibility = 'visible';
const ov = document.getElementById('loadingOverlay'); if (ov) ov.style.display = 'flex';
@@ -825,6 +889,9 @@ function bindDarkMode() {
window.__FR_AUTH_STATE = state;
} catch { }
// authed → heavy boot path
document.body.classList.add('authed');
// 1) i18n (safe)
// i18n: honor saved language first, then apply translations
try {
@@ -840,10 +907,20 @@ function bindDarkMode() {
if (!window.__FR_FLAGS.initialized) {
if (typeof app.loadCsrfToken === 'function') await app.loadCsrfToken();
if (typeof app.initializeApp === 'function') app.initializeApp();
const darkBtn = document.getElementById('darkModeToggle');
if (darkBtn) {
darkBtn.removeAttribute('hidden');
darkBtn.style.setProperty('display', 'inline-flex', 'important'); // beats any CSS
darkBtn.style.visibility = ''; // just in case
}
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/vendor/material-icons.css?v={{APP_QVER}}';
document.head.appendChild(link);
window.__FR_FLAGS.initialized = true;
// Show "Welcome back, <username>!" only once per tab-session
try {
if (!sessionStorage.getItem('__fr_welcomed')) {
const name = (window.__FR_AUTH_STATE?.username) || localStorage.getItem('username') || '';
@@ -864,7 +941,7 @@ function bindDarkMode() {
auth.applyProxyBypassUI && auth.applyProxyBypassUI();
auth.updateAuthenticatedUI && auth.updateAuthenticatedUI(state);
// ⬇️ bind ALL the admin / change-password buttons once
// bind ALL the admin / change-password buttons once
if (!window.__FR_FLAGS.wired.authInit && typeof auth.initAuth === 'function') {
try { auth.initAuth(); } catch (e) { console.warn('[auth] initAuth failed', e); }
window.__FR_FLAGS.wired.authInit = true;
@@ -913,36 +990,71 @@ function bindDarkMode() {
// ---------- entry (no flicker: decide state BEFORE showing login) ----------
document.addEventListener('DOMContentLoaded', async () => {
if (window.__FR_FLAGS.entryStarted) return;
window.__FR_FLAGS.entryStarted = true;
// Always start clean
document.body.classList.remove('authed');
const overlay = document.getElementById('loadingOverlay');
const wrap = document.querySelector('.main-wrapper'); // app shell
const mainEl = document.getElementById('main'); // contains loginForm
const login = document.getElementById('loginForm');
bindDarkMode();
await loadSiteConfig();
const { authed, setup } = await checkAuth();
if (setup) { await bootSetupWizard(); return; }
if (authed) { await bootHeavy(); return; }
if (setup) {
// Setup wizard runs inside app shell
unhide(wrap);
hideEl(login);
await bootSetupWizard();
await revealAppAndHideOverlay();
// login view
show(document.querySelector('#main'));
show(document.querySelector('#loginForm'));
(document.querySelector('.header-buttons') || {}).style && (document.querySelector('.header-buttons').style.visibility = 'hidden');
const ov = document.getElementById('loadingOverlay'); if (ov) ov.style.display = 'none';
return;
}
if (authed) {
// Authenticated path: show app, hide login
document.body.classList.add('authed');
unhide(wrap); // works whether CSS or [hidden] was used
hideEl(login);
await bootHeavy();
await revealAppAndHideOverlay();
requestAnimationFrame(() => {
const pre = document.getElementById('pretheme-css');
if (pre) pre.remove();
});
return;
}
// ---- NOT AUTHED: show only the login view ----
hideEl(wrap); // ensure app shell stays hidden while logged out
unhide(mainEl);
unhide(login);
if (login) login.style.display = '';
// …wire stuff…
applySiteConfig(window.__FR_SITE_CFG__ || {}, { phase: 'final' });
await revealAppAndHideOverlay();
const hb = document.querySelector('.header-buttons');
if (hb) hb.style.visibility = 'hidden';
// keep app cards inert while logged out (no layout poke)
['uploadCard', 'folderManagementCard'].forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.style.display = 'none';
el.setAttribute('aria-hidden', 'true');
try { el.inert = true; } catch { }
});
bindLogin();
wireCreateDropdown();
keepCreateDropdownWired();
wireModalEnterDefault();
showLoginTip('Please log in to continue');
}, { once: true }); // <— important
if (overlay) overlay.style.display = 'none';
}, { once: true });
})();