release(v1.9.8): feat(pro): wire core to Pro licensing + branding hooks

This commit is contained in:
Ryan
2025-11-16 21:11:06 -05:00
committed by GitHub
parent 9880adb417
commit 060a548af4
9 changed files with 1308 additions and 106 deletions

View File

@@ -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")} <small style="font-size:12px;color:gray;">${version}</small>`;
function getAdminTitle(isPro, proVersion) {
const corePill = `
<span class="badge badge-pill badge-secondary admin-core-badge">
Core ${version}
</span>
`;
if (!isPro) {
// Free/core only
return `
${t("admin_panel")}
${corePill}
`;
}
const pv = proVersion ? `Pro v${proVersion}` : 'Pro';
const proPill = `
<span class="badge badge-pill badge-warning admin-pro-badge">
${pv}
</span>
`;
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 = `
<div class="modal-content" style="${inner}">
<div class="editor-close-btn" id="closeAdminPanel">&times;</div>
<h3>${adminTitle}</h3>
<h3>${getAdminTitle(isPro, proVersion)}</h3>
<form id="adminPanelForm">
${[
{ id: "userManagement", label: t("user_management") },
@@ -495,6 +592,7 @@ export function openAdminPanel() {
{ id: "upload", label: t("shared_max_upload_size_bytes_title") },
{ id: "oidc", label: t("oidc_configuration") + " & TOTP" },
{ id: "shareLinks", label: t("manage_shared_links") },
{ id: "pro", label: "FileRise Pro" },
{ id: "sponsor", label: (typeof tf === 'function' ? tf("sponsor_donations", "Sponsor / Donations") : "Sponsor / Donations") }
].map(sec => `
<div id="${sec.id}Header" class="section-header collapsed">
@@ -515,18 +613,100 @@ export function openAdminPanel() {
document.getElementById("closeAdminPanel").addEventListener("click", closeAdminPanel);
document.getElementById("cancelAdminSettings").addEventListener("click", closeAdminPanel);
["userManagement", "headerSettings", "loginOptions", "webdav", "onlyoffice", "upload", "oidc", "shareLinks", "sponsor"]
["userManagement", "headerSettings", "loginOptions", "webdav", "onlyoffice", "upload", "oidc", "shareLinks", "pro", "sponsor"]
.forEach(id => {
document.getElementById(id + "Header")
.addEventListener("click", () => toggleSection(id));
});
document.getElementById("userManagementContent").innerHTML = `
<button type="button" id="adminOpenAddUser" class="btn btn-success me-2">${t("add_user")}</button>
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger me-2">${t("remove_user")}</button>
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${tf("folder_access", "Folder Access")}</button>
<button type="button" id="adminOpenUserFlags" class="btn btn-secondary">${tf("user_permissions", "User Permissions")}</button>
`;
document.getElementById("userManagementContent").innerHTML = `
<div class="admin-user-actions">
<!-- Core buttons -->
<button type="button" id="adminOpenAddUser" class="btn btn-success btn-sm">
<i class="material-icons">person_add</i>
<span>${t("add_user")}</span>
</button>
<button type="button" id="adminOpenRemoveUser" class="btn btn-danger btn-sm">
<i class="material-icons">person_remove</i>
<span>${t("remove_user")}</span>
</button>
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary btn-sm">
<i class="material-icons">folder_shared</i>
<span>${tf("folder_access", "Folder Access")}</span>
</button>
<button type="button" id="adminOpenUserFlags" class="btn btn-secondary btn-sm">
<i class="material-icons">tune</i>
<span>${tf("user_permissions", "User Permissions")}</span>
</button>
<!-- Pro-only: User groups -->
${
isPro
? `
<div class="btn-pro-wrapper">
<button
type="button"
id="adminOpenUserGroups"
class="btn btn-sm btn-pro-admin">
<i class="material-icons">groups</i>
<span>User groups</span>
</button>
</div>
`
: `
<div class="btn-pro-wrapper">
<button
type="button"
id="adminOpenUserGroups"
class="btn btn-sm btn-pro-admin">
<i class="material-icons">groups</i>
<span>User groups</span>
</button>
<span class="btn-pro-pill">Pro · Coming soon</span>
</div>
`
}
<!-- Pro roadmap: Client portal -->
${
isPro
? `
<div class="btn-pro-wrapper">
<button
type="button"
id="adminOpenClientPortal"
class="btn btn-sm btn-pro-admin"
title="Client upload portals are part of FileRise Pro.">
<i class="material-icons">cloud_upload</i>
<span>Client upload portal</span>
</button>
</div>
`
: `
<div class="btn-pro-wrapper">
<button
type="button"
id="adminOpenClientPortal"
class="btn btn-sm btn-pro-admin"
disabled
title="Planned FileRise Pro feature: client upload portals">
<i class="material-icons">cloud_upload</i>
<span>Client upload portal</span>
</button>
<span class="btn-pro-pill">Pro · Coming soon</span>
</div>
`
}
</div>
<small class="text-muted d-block" style="margin-top:6px;">
Use the core tools to manage users and per-folder access.
User groups and Client upload portals are planned FileRise Pro features.
</small>
`;
document.getElementById("adminOpenAddUser")
.addEventListener("click", () => {
@@ -541,13 +721,174 @@ export function openAdminPanel() {
document.getElementById("adminOpenUserPermissions")
.addEventListener("click", openUserPermissionsModal);
document.getElementById("headerSettingsContent").innerHTML = `
<div class="form-group">
<label for="headerTitle">${t("header_title_text")}:</label>
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle || ""}" />
</div>
`;
wireHeaderTitleLive();
// Pro-only stubs for future features
const regBtn = document.getElementById("adminOpenUserRegistration");
const groupsBtn = document.getElementById("adminOpenUserGroups");
const clientBtn = document.getElementById("adminOpenClientPortal");
if (regBtn) {
regBtn.addEventListener("click", () => {
if (!isPro) {
showToast("User registration is a FileRise Pro feature. Visit filerise.net to purchase a license.");
window.open("https://filerise.net", "_blank", "noopener");
return;
}
// Placeholder for future Pro UI:
showToast("User registration management is coming soon in FileRise Pro.");
});
}
if (groupsBtn) {
groupsBtn.addEventListener("click", () => {
if (!isPro) {
showToast("User groups are a FileRise Pro feature. Visit filerise.net to purchase a license.");
window.open("https://filerise.net", "_blank", "noopener");
return;
}
// Placeholder for future Pro UI:
showToast("User groups management is coming soon in FileRise Pro.");
});
}
if (clientBtn) {
clientBtn.addEventListener("click", () => {
if (!isPro) {
showToast("Client portal uploads are a FileRise Pro feature. Visit filerise.net to purchase a license.");
window.open("https://filerise.net", "_blank", "noopener");
return;
}
// Placeholder for future Pro UI:
showToast("Client portal uploads are coming soon in FileRise Pro.");
});
}
document.getElementById("headerSettingsContent").innerHTML = `
<div class="form-group">
<label for="headerTitle">${t("header_title_text")}:</label>
<input type="text" id="headerTitle" class="form-control" value="${window.headerTitle || ""}" />
</div>
<!-- Pro: Logo -->
<div class="form-group" style="margin-top:16px;">
<label for="brandingCustomLogoUrl">
Header Logo
${!isPro ? '<span class="badge badge-pill badge-warning admin-pro-badge" style="margin-left:6px;">Pro</span>' : ''}
</label>
<small class="text-muted d-block mb-1">
${isPro
? 'Upload a logo image or paste a local path.'
: 'Requires FileRise Pro to enable custom header branding.'}
</small>
<div class="input-group mb-2">
<input
type="text"
id="brandingCustomLogoUrl"
class="form-control"
placeholder="/uploads/profile_pics/logo.png"
value="${isPro ? (brandingCustomLogoUrl.replace(/"/g, '&quot;')) : ''}"
${!isPro ? 'disabled data-disabled-reason="pro"' : ''}
/>
</div>
<div class="input-group">
<input
type="file"
id="brandingLogoFile"
class="form-control"
accept="image/*"
${!isPro ? 'disabled' : ''}
/>
<button
type="button"
class="btn btn-sm btn-secondary"
id="brandingUploadBtn"
${!isPro ? 'disabled' : ''}>
Upload logo
</button>
</div>
</div>
<!-- Pro: Header colors -->
<div class="form-group" style="margin-top:16px;">
<label>
Header Colors
${!isPro ? '<span class="badge badge-pill badge-warning admin-pro-badge" style="margin-left:6px;">Pro</span>' : ''}
</label>
<div class="d-flex align-items-center" style="gap: 12px; flex-wrap: wrap;">
<div>
<label for="brandingHeaderBgLight" class="d-block" style="font-size: 12px; margin-bottom: 4px;">Light mode</label>
<input
type="color"
id="brandingHeaderBgLight"
value="${brandingHeaderBgLight || '#2196F3'}"
${!isPro ? 'disabled' : ''}
/>
</div>
<div>
<label for="brandingHeaderBgDark" class="d-block" style="font-size: 12px; margin-bottom: 4px;">Dark mode</label>
<input
type="color"
id="brandingHeaderBgDark"
value="${brandingHeaderBgDark || '#1f1f1f'}"
${!isPro ? 'disabled' : ''}
/>
</div>
</div>
<small class="text-muted d-block mt-1">
${isPro
? 'If left empty, FileRise uses its default blue and dark header colors.'
: 'Requires FileRise Pro to enable custom color branding.'}
</small>
</div>
`;
wireHeaderTitleLive();
// Upload logo -> reuse profile picture endpoint, then fill the logo path
if (isPro) {
const fileInput = document.getElementById('brandingLogoFile');
const uploadBtn = document.getElementById('brandingUploadBtn');
const urlInput = document.getElementById('brandingCustomLogoUrl');
if (fileInput && uploadBtn && urlInput) {
uploadBtn.addEventListener('click', async () => {
const f = fileInput.files && fileInput.files[0];
if (!f) {
showToast('Please choose an image first.');
return;
}
const fd = new FormData();
fd.append('brand_logo', f); // <- must match PHP field
try {
const res = await fetch('/api/pro/uploadBrandLogo.php', {
method: 'POST',
credentials: 'include',
headers: { 'X-CSRF-Token': window.csrfToken },
body: fd
});
const text = await res.text();
let js = {};
try { js = JSON.parse(text || '{}'); } catch (e) { js = {}; }
if (!res.ok || !js.url) {
showToast(js.error || 'Error uploading logo');
return;
}
const normalized = normalizeLogoPath(js.url); // your helper
urlInput.value = normalized;
showToast('Logo uploaded. Don\'t forget to Save settings.');
} catch (e) {
console.error(e);
showToast('Error uploading logo');
}
});
}
}
document.getElementById("loginOptionsContent").innerHTML = `
<div class="form-group"><input type="checkbox" id="disableFormLogin" /> <label for="disableFormLogin">${t("disable_login_form")}</label></div>
@@ -912,6 +1253,236 @@ async function ooProbeFrame(docsOrigin, timeoutMs = 4000) {
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
// --- FileRise Pro / License section ---
const proContent = document.getElementById("proContent");
if (proContent) {
const proMetaHtml =
isPro && (proType || proEmail || proVersion)
? `
<div class="pro-license-meta">
<div>
${proType ? `License type: ${proType}` : 'License active'}
${proType && proEmail ? ' • ' : ''}
${proEmail ? `Licensed to: ${proEmail}` : ''}
</div>
<div>
Pro bundle version: v${proVersion}
</div>
</div>
`
: '';
proContent.innerHTML = `
<div class="card pro-card" style="padding:12px; border:1px solid #ddd; border-radius:12px; max-width:620px; margin:8px auto;">
<div>
<!-- Title row with pill aligned to "FileRise Pro" -->
<div class="d-flex align-items-center" style="gap:8px;">
<strong>FileRise Pro</strong>
<span class="badge badge-pill ${isPro ? 'badge-success' : 'badge-secondary'} admin-pro-badge">
${isPro ? 'Active' : 'Free'}
</span>
</div>
<!-- Subtitle + meta under the title -->
<div style="font-size:12px; color:#777; margin-top:2px;">
${isPro
? 'Pro features are currently enabled on this instance.'
: 'You are running the free edition. Enter a license key to activate FileRise Pro.'}
</div>
${proMetaHtml}
</div>
${isPro ? `
<div style="margin-top:8px;">
<a
href="https://filerise.net/pro/update.php"
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-pro-admin">
Download latest Pro bundle
</a>
<small class="text-muted d-block" style="margin-top:4px;">
Opens filerise.net in a new tab where you can enter your Pro license
to download the latest FileRise Pro ZIP.
</small>
</div>
` : `
<div style="margin-top:8px;">
<a
href="https://filerise.net/pro/checkout.php"
target="_blank"
rel="noopener noreferrer"
class="btn btn-sm btn-pro-admin">
Buy FileRise Pro
</a>
<small class="text-muted d-block" style="margin-top:4px;">
Opens filerise.net in a new tab so you can purchase a FileRise Pro license.
</small>
</div>
`}
<div class="form-group" style="margin-top:10px;">
<label for="proLicenseInput" style="font-size:12px;">License key</label>
<textarea
id="proLicenseInput"
class="form-control"
rows="3"
placeholder="Paste your FileRise Pro license key here..."></textarea>
<small class="text-muted">
You can purchase a license at
<a href="https://filerise.net" target="_blank" rel="noopener noreferrer">filerise.net</a>.
</small>
</div>
${isPro && proLicense ? `
<div style="margin-top:6px;">
<button type="button" class="btn btn-secondary btn-sm" id="proCopyLicenseBtn">
Copy current license
</button>
<small class="text-muted d-block" style="margin-top:4px;">
Copies the saved license so you can reuse it for upgrades or downloads on filerise.net.
</small>
</div>
` : ''}
<div class="form-group" style="margin-top:6px;">
<label style="font-size:12px;">Or upload license file</label>
<div class="input-group">
<input
type="file"
id="proLicenseFile"
class="form-control"
accept=".lic,.json,.txt,.filerise-lic"
/>
<button type="button" class="btn btn-sm btn-secondary" id="proLoadLicenseFileBtn">
Load from file
</button>
</div>
<small class="text-muted">
Supported: FileRise.lic, plain text with FRP1... or JSON containing a <code>license</code> field.
</small>
</div>
<button type="button" class="btn btn-primary btn-sm" id="proSaveLicenseBtn" style="margin-top:8px;">
Save license
</button>
</div>
`;
// Pre-fill textarea with saved license if present
const licenseTextarea = document.getElementById('proLicenseInput');
if (licenseTextarea && proLicense) {
licenseTextarea.value = proLicense;
}
// File upload → fill textarea (unchanged)
const fileInput = document.getElementById('proLicenseFile');
const fileBtn = document.getElementById('proLoadLicenseFileBtn');
if (fileInput && fileBtn && licenseTextarea) {
fileBtn.addEventListener('click', () => {
const file = fileInput.files && fileInput.files[0];
if (!file) {
showToast('Please choose a license file first.');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
let raw = String(e.target.result || '').trim();
let license = raw;
try {
const js = JSON.parse(raw);
if (js && typeof js.license === 'string') {
license = js.license.trim();
}
} catch (_) {
// not JSON, treat as plain text
}
if (!license || !license.startsWith('FRP1.')) {
showToast('Could not find a valid FRP1 license in that file.');
return;
}
licenseTextarea.value = license;
showToast('License loaded from file. Click "Save license" to apply.');
};
reader.onerror = () => {
showToast('Error reading license file.');
};
reader.readAsText(file);
});
}
// Copy current license button
const proCopyBtn = document.getElementById('proCopyLicenseBtn');
if (proCopyBtn && proLicense) {
proCopyBtn.addEventListener('click', async () => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(proLicense);
} else {
const ta = document.createElement('textarea');
ta.value = proLicense;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
}
showToast('License copied to clipboard.');
} catch {
showToast('Could not copy license. Please copy it manually.');
}
});
}
// Save license handler (unchanged)
const proSaveBtn = document.getElementById('proSaveLicenseBtn');
if (proSaveBtn) {
proSaveBtn.addEventListener('click', async () => {
const ta = document.getElementById('proLicenseInput');
const license = (ta && ta.value.trim()) || '';
try {
const res = await fetch('/api/admin/setLicense.php', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]')?.content || '')
},
body: JSON.stringify({ license }),
});
const text = await res.text();
let data = {};
try { data = JSON.parse(text || '{}'); } catch (e) { data = {}; }
if (!res.ok || !data.success) {
console.error('setLicense error:', res.status, text);
showToast(data.error || 'Error saving license');
return;
}
showToast('License saved. Reloading…');
window.location.reload();
} catch (e) {
console.error(e);
showToast('Error saving license');
}
});
}
}
// --- end FileRise Pro section ---
document.getElementById("saveAdminSettings")
.addEventListener("click", handleSave);
["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => {
@@ -1065,6 +1636,11 @@ function handleSave() {
// clientId/clientSecret: only include when replacing
},
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim(),
branding: {
customLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
headerBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
headerBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
},
};
const idEl = document.getElementById("oidcClientId");
@@ -1119,6 +1695,8 @@ function handleSave() {
if (j.error) { showToast('Error: ' + j.error); return; }
showToast('Settings saved.');
closeAdminPanel();
applyHeaderColorsFromAdmin();
updateHeaderLogoFromAdmin();
})
.catch(() => showToast('Save failed.'));
}