From 060a548af458d16bdb0b0e530b5e087234ba727d Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 16 Nov 2025 21:11:06 -0500 Subject: [PATCH] release(v1.9.8): feat(pro): wire core to Pro licensing + branding hooks --- CHANGELOG.md | 70 ++++ config/config.php | 30 +- public/api/admin/setLicense.php | 8 + public/css/styles.css | 134 +++++- public/js/adminPanel.js | 616 +++++++++++++++++++++++++++- public/js/main.js | 34 ++ src/controllers/AdminController.php | 348 ++++++++++++---- src/controllers/UserController.php | 82 +++- src/models/AdminModel.php | 92 +++++ 9 files changed, 1308 insertions(+), 106 deletions(-) create mode 100644 public/api/admin/setLicense.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fca0e..3363eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ # Changelog +## Changes 11/16/2025 (v1.9.8) + +release(v1.9.8): feat(pro): wire core to Pro licensing + branding hooks + +- Add Pro feature flags + bootstrap wiring + - Define FR_PRO_ACTIVE/FR_PRO_TYPE/FR_PRO_EMAIL/FR_PRO_VERSION/FR_PRO_LICENSE_FILE + in config.php and optionally require src/pro/bootstrap_pro.php. + - Expose a `pro` block from AdminController::getConfig() so the UI can show + license status, type, email, and bundle version without leaking the raw key. + +- Implement license save endpoint + - Add AdminController::setLicense() and /api/admin/setLicense.php to accept a + FRP1 license string via JSON, validate basic shape, and persist it to + FR_PRO_LICENSE_FILE with strict 0600 permissions. + - Return structured JSON success/error responses for the admin UI. + +- Extend admin config model with branding + safer validation + - Add `branding.customLogoUrl`, `branding.headerBgLight`, and + `branding.headerBgDark` fields to AdminModel defaults and updateConfig(). + - Introduce AdminModel::sanitizeLogoUrl() to allow only site-relative /uploads + paths or http(s) URLs; reject absolute filesystem paths, data: URLs, and + javascript: URLs. + - Continue to validate ONLYOFFICE docsOrigin as http(s) only, keeping core + config hardening intact. + +- New Pro-aware Admin Panel UI + - Rework User Management section to group: + - Add user / Remove user + - Folder Access (per-folder ACL) + - User Permissions (account-level flags) + - Add Pro-only actions with clear gating: + - “User groups” button (Pro) + - “Client upload portal” button with “Pro · Coming soon” pill + - Add “FileRise Pro” section: + - Show current Pro status (Free vs Active) + license metadata. + - Textarea for pasting license key, file upload helper, and “Save license” + action wired to /api/admin/setLicense.php. + - Optional “Copy current license” button when a license is present. + - Add “Sponsor / Donations” section with fixed GitHub Sponsors and Ko-fi URLs + and one-click copy/open buttons. + +- Header branding controls (Pro) + - Add Header Logo + Header Colors controls under Header Settings, gated by + `config.pro.active`. + - Allow uploading a logo via /api/pro/uploadBrandLogo.php and auto-filling the + normalized /uploads path. + - Add live-preview helpers to update the header logo and header background + colors in the running UI after saving. + +- Apply branding on app boot + - Update main.js to read branding config on load and apply: + - Custom header logo (or fallback to /assets/logo.svg). + - Light/dark header background colors via CSS variables. + - Keeps header consistent with saved branding across reloads and before + opening the admin panel. + +- Styling + UX polish + - Add styles for new admin sections: collapsible headers, dark-mode aware + modal content, and refined folder access grid. + - Introduce .btn-pro-admin and .btn-pro-pill classes to render “Pro” and + “Pro · Coming soon” pills overlayed on buttons, matching the existing + header “Core/Pro” badge treatment. + - Minor spacing/typography tweaks in admin panel and ACL UI. + +Note: Core code remains MIT-licensed; Pro functionality is enabled via optional +runtime hooks and separate closed-source bundle, without changing the core +license text. + +--- + ## Changes 11/14/2025 (v1.9.7) release(v1.9.7): harden client path guard and refine header/folder strip CSS diff --git a/config/config.php b/config/config.php index 4dc2279..98a0377 100644 --- a/config/config.php +++ b/config/config.php @@ -238,4 +238,32 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) { } // Final: env var wins, else fallback -define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare); \ No newline at end of file +define('SHARE_URL', getenv('SHARE_URL') ?: $defaultShare); + +// -------------------------------- +// FileRise Pro (optional add-on) +// -------------------------------- + +// Where the Pro license JSON lives +if (!defined('PRO_LICENSE_FILE')) { + define('PRO_LICENSE_FILE', PROJECT_ROOT . '/users/proLicense.json'); +} + +// Inline/env license strings (optional) +if (!defined('FR_PRO_LICENSE')) { + define('FR_PRO_LICENSE', getenv('FR_PRO_LICENSE') ?: ''); +} +if (!defined('FR_PRO_LICENSE_FILE')) { + define('FR_PRO_LICENSE_FILE', getenv('FR_PRO_LICENSE_FILE') ?: ''); +} + +// Optional Pro bootstrap (shipped only with Pro bundle) +$proBootstrap = PROJECT_ROOT . '/src/pro/bootstrap_pro.php'; +if (is_file($proBootstrap)) { + require_once $proBootstrap; +} + +// Safe default so the rest of the app always has the constant +if (!defined('FR_PRO_ACTIVE')) { + define('FR_PRO_ACTIVE', false); +} diff --git a/public/api/admin/setLicense.php b/public/api/admin/setLicense.php new file mode 100644 index 0000000..4b80f18 --- /dev/null +++ b/public/api/admin/setLicense.php @@ -0,0 +1,8 @@ +setLicense(); \ No newline at end of file diff --git a/public/css/styles.css b/public/css/styles.css index 4a49496..7e0ec1f 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -67,17 +67,129 @@ body{letter-spacing: 0.2px; font-size: 34px !important; color: red !important; transform: translateY(-3px) !important;} -.header-container{display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 55px; - padding: 10px 20px; - background-color: #2196F3; - transition: background-color 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);} -.dark-mode .header-container{background-color: #1f1f1f; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7);} + .header-container{ + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 55px; + padding: 10px 20px; + background-color: var(--header-bg-light, #2196F3); + transition: background-color 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + .dark-mode .header-container{ + background-color: var(--header-bg-dark, #1f1f1f); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.7); + } + + .admin-core-badge, +.admin-pro-badge { + font-size: 12px; + padding: 2px 6px; + vertical-align: middle; + position: relative; + top: -1px; + margin-left: 6px; +} + + #adminPanelModal .section-content .pro-card { + margin: 8px 10px; + border-radius: 12px; + } + #adminPanelModal .section-content { + margin: 0px 10px; + } + .pro-license-meta { + margin-top: 6px; + padding: 6px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 500; + background-color: rgba(40, 167, 69, 0.08); /* light green tint */ + border: 1px solid rgba(40, 167, 69, 0.4); + color: #2e7d32; + } + + .pro-license-meta div + div { + margin-top: 2px; + } + + /* Dark mode tweak so it doesn't glow too bright */ + .dark-mode .pro-license-meta { + background-color: rgba(40, 167, 69, 0.18); + border-color: rgba(40, 167, 69, 0.6); + color: #c8e6c9; + } + /* FileRise Pro button styling (admin) */ +.btn-pro-admin { + background: linear-gradient(135deg, #ff9800, #ff5722); + border-color: #ff9800; + color: #1b0f00 !important; + font-weight: 600; + box-shadow: 0 0 10px rgba(255, 152, 0, 0.4); +} + +.btn-pro-admin:hover { + filter: brightness(1.05); +} + + /* User management action bar */ + .admin-user-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 4px; + } + .admin-user-actions .btn { + border-radius: 999px; + font-size: 12px; + padding: 6px 12px; + display: inline-flex; + align-items: center; + gap: 4px; + } + .admin-user-actions .btn .material-icons { + font-size: 16px; + line-height: 1; + } + + /* ---------- Pro buttons + pill ---------- */ + .admin-user-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 6px; + } + + .btn-pro-wrapper { + position: relative; + display: inline-block; + } + .btn-pro-admin:disabled { + opacity: 0.9; + } + + + .btn-pro-pill { + position: absolute; + top: -7px; + right: -4px; + font-size: 10px; + line-height: 1.2; + padding: 2px 6px; + border-radius: 999px; + background: #ffc107; + color: black; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + pointer-events: none; + white-space: nowrap; + font-weight: 600; + } + #userManagementContent { + margin-top: 10px !important; + } + #darkModeIcon{color: #fff;} .header-logo{max-height: 50px; width: auto; diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index d892b68..7648ae0 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -4,8 +4,46 @@ import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js?v={{APP_QVER}}'; import { sendRequest } from './networkUtils.js?v={{APP_QVER}}'; +function normalizeLogoPath(raw) { + if (!raw) return ''; + const parts = String(raw).split(':'); + let pic = parts[parts.length - 1]; + pic = pic.replace(/^:+/, ''); + if (pic && !pic.startsWith('/')) pic = '/' + pic; + return pic; +} + const version = window.APP_VERSION || "dev"; -const adminTitle = `${t("admin_panel")} ${version}`; + +function getAdminTitle(isPro, proVersion) { + const corePill = ` + + Core ${version} + + `; + + if (!isPro) { + // Free/core only + return ` + ${t("admin_panel")} + ${corePill} + `; + } + + const pv = proVersion ? `Pro v${proVersion}` : 'Pro'; + + const proPill = ` + + ${pv} + + `; + + return ` + ${t("admin_panel")} + ${corePill} + ${proPill} + `; +} function buildFullGrantsForAllFolders(folders) { @@ -16,6 +54,49 @@ function buildFullGrantsForAllFolders(folders) { }; return folders.reduce((acc, f) => { acc[f] = { ...allTrue }; return acc; }, {}); } +function applyHeaderColorsFromAdmin() { + try { + const lightInput = document.getElementById('brandingHeaderBgLight'); + const darkInput = document.getElementById('brandingHeaderBgDark'); + const root = document.documentElement; + + const light = lightInput ? lightInput.value.trim() : ''; + const dark = darkInput ? darkInput.value.trim() : ''; + + if (light) root.style.setProperty('--header-bg-light', light); + else root.style.removeProperty('--header-bg-light'); + + if (dark) root.style.setProperty('--header-bg-dark', dark); + else root.style.removeProperty('--header-bg-dark'); + } catch (e) { + console.warn('Failed to live-update header colors from admin panel', e); + } +} +function updateHeaderLogoFromAdmin() { + try { + const input = document.getElementById('brandingCustomLogoUrl'); + const logoImg = document.querySelector('.header-logo img'); + if (!logoImg) return; + + let url = (input && input.value.trim()) || ''; + + // If they used a bare "uploads/..." path, normalize to "/uploads/..." + if (url && !url.startsWith('/') && url.startsWith('uploads/')) { + url = '/' + url; + } + + if (url) { + logoImg.setAttribute('src', url); + logoImg.setAttribute('alt', 'Site logo'); + } else { + // fall back to default FileRise logo + logoImg.setAttribute('src', '/assets/logo.svg?v={{APP_QVER}}'); + logoImg.setAttribute('alt', 'FileRise'); + } + } catch (e) { + console.warn('Failed to live-update header logo from admin panel', e); + } +} /* === BEGIN: Folder Access helpers (merged + improved) === */ function qs(scope, sel) { return (scope || document).querySelector(sel); } @@ -175,7 +256,7 @@ async function safeJson(res) { .dark-mode .form-control::placeholder { color:#888; } .section-header { - background:#f5f5f5; padding:10px 15px; cursor:pointer; border-radius:4px; font-weight:bold; + background:#f5f5f5; padding:10px 15px; cursor:pointer; border-radius:12px; font-weight:bold; display:flex; align-items:center; justify-content:space-between; margin-top:16px; } .section-header:first-of-type { margin-top:0; } @@ -301,7 +382,10 @@ function captureInitialAdminConfig() { disableOIDCLogin: !!document.getElementById("disableOIDCLogin")?.checked, enableWebDAV: !!document.getElementById("enableWebDAV")?.checked, sharedMaxUploadSize: (document.getElementById("sharedMaxUploadSize")?.value || "").trim(), - globalOtpauthUrl: (document.getElementById("globalOtpauthUrl")?.value || "").trim() + globalOtpauthUrl: (document.getElementById("globalOtpauthUrl")?.value || "").trim(), + brandingCustomLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(), + brandingHeaderBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(), + brandingHeaderBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(), }; } function hasUnsavedChanges() { @@ -319,7 +403,10 @@ function hasUnsavedChanges() { getChk("disableOIDCLogin") !== o.disableOIDCLogin || getChk("enableWebDAV") !== o.enableWebDAV || getVal("sharedMaxUploadSize") !== o.sharedMaxUploadSize || - getVal("globalOtpauthUrl") !== o.globalOtpauthUrl + getVal("globalOtpauthUrl") !== o.globalOtpauthUrl || + getVal("brandingCustomLogoUrl") !== (o.brandingCustomLogoUrl || "") || + getVal("brandingHeaderBgLight") !== (o.brandingHeaderBgLight || "") || + getVal("brandingHeaderBgDark") !== (o.brandingHeaderBgDark || "") ); } @@ -460,6 +547,16 @@ export function openAdminPanel() { if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl; const dark = document.body.classList.contains("dark-mode"); + const proInfo = config.pro || {}; + const isPro = !!proInfo.active; + const proType = proInfo.type || ''; + const proEmail = proInfo.email || ''; + const proVersion = proInfo.version || 'not installed'; + const proLicense = proInfo.license || ''; + const brandingCfg = config.branding || {}; + const brandingCustomLogoUrl = brandingCfg.customLogoUrl || ""; + const brandingHeaderBgLight = brandingCfg.headerBgLight || ""; + const brandingHeaderBgDark = brandingCfg.headerBgDark || ""; const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; const inner = ` background:${dark ? "#2c2c2c" : "#fff"}; @@ -484,7 +581,7 @@ export function openAdminPanel() { mdl.innerHTML = `