Admin Panel Refactor & Enhancements
This commit is contained in:
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 5/3/2025 v1.3.0
|
||||||
|
|
||||||
|
**Admin Panel Refactor & Enhancements**
|
||||||
|
|
||||||
|
### Moved from `authModals.js` to `adminPanel.js`
|
||||||
|
|
||||||
|
- Extracted all admin-related UI and logic out of `authModals.js`
|
||||||
|
- Created a standalone `adminPanel.js` module
|
||||||
|
- Initialized `openAdminPanel()` and `closeAdminPanel()` exports
|
||||||
|
|
||||||
|
### Responsive, Collapsible Sections
|
||||||
|
|
||||||
|
- Injected new CSS via JS (`adminPanelStyles`)
|
||||||
|
- Default modal width: 50%
|
||||||
|
- Small-screen override (`@media (max-width: 600px)`) to 90% width
|
||||||
|
- Introduced `.section-header` / `.section-content` pattern
|
||||||
|
- Click header to expand/collapse its content
|
||||||
|
- Animated arrow via Material Icons
|
||||||
|
- Indented and padded expanded content
|
||||||
|
|
||||||
|
### “Manage Shared Links” Feature
|
||||||
|
|
||||||
|
- Added new **Manage Shared Links** section to Admin Panel
|
||||||
|
- Endpoint **GET** `/api/admin/readMetadata.php?file=…`
|
||||||
|
- Reads `share_folder_links.json` & `share_links.json` under `META_DIR`
|
||||||
|
- Endpoint **POST**
|
||||||
|
- `/api/folder/deleteShareFolderLink.php`
|
||||||
|
- `/api/file/deleteShareLink.php`
|
||||||
|
- `loadShareLinksSection()` AJAX loader
|
||||||
|
- Displays folder & file shares, expiry dates, upload-allowed, and 🔒 if password-protected
|
||||||
|
- “🗑️” delete buttons refresh the list on success
|
||||||
|
|
||||||
|
### Dark-Mode & Theming Fixes
|
||||||
|
|
||||||
|
- Dark-mode CSS overrides for:
|
||||||
|
- Modal border
|
||||||
|
- `.btn-primary`, `.btn-secondary`
|
||||||
|
- `.form-control` backgrounds & placeholders
|
||||||
|
- Section headers & icons
|
||||||
|
- Close button restyled to use shared **.editor-close-btn** look
|
||||||
|
|
||||||
|
### API and Controller changes
|
||||||
|
|
||||||
|
- Updated all endpoints to use correct controller casing
|
||||||
|
- Renamed controller files to PascalCase (e.g. `adminController.php` to `AdminController.php`, `fileController.php` to `FileController.php`, `folderController.php` to `FolderController.php`)
|
||||||
|
- Adjusted endpoint paths to match controller filenames
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 4/30/2025 v1.2.8
|
## Changes 4/30/2025 v1.2.8
|
||||||
|
|
||||||
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.
|
- **Added** PDF preview in `filePreview.js` (the `extension === "pdf"` block): replaced in-modal `<embed>` with `window.open(urlWithTs, "_blank")` and closed the modal to avoid CSP `frame-ancestors 'none'` restrictions.
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ const version = "v1.3.0";
|
|||||||
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>`;
|
||||||
|
|
||||||
// ————— Inject updated styles —————
|
// ————— Inject updated styles —————
|
||||||
(function(){
|
(function () {
|
||||||
if (document.getElementById('adminPanelStyles')) return;
|
if (document.getElementById('adminPanelStyles')) return;
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = 'adminPanelStyles';
|
style.id = 'adminPanelStyles';
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
/* Modal sizing */
|
/* Modal sizing */
|
||||||
#adminPanelModal .modal-content {
|
#adminPanelModal .modal-content {
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
@@ -112,24 +112,24 @@ const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;
|
|||||||
margin-top:15px;
|
margin-top:15px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
})();
|
})();
|
||||||
// ————————————————————————————————————
|
// ————————————————————————————————————
|
||||||
|
|
||||||
let originalAdminConfig = {};
|
let originalAdminConfig = {};
|
||||||
function captureInitialAdminConfig() {
|
function captureInitialAdminConfig() {
|
||||||
originalAdminConfig = {
|
originalAdminConfig = {
|
||||||
headerTitle: document.getElementById("headerTitle").value.trim(),
|
headerTitle: document.getElementById("headerTitle").value.trim(),
|
||||||
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
oidcProviderUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||||
oidcClientId: document.getElementById("oidcClientId").value.trim(),
|
oidcClientId: document.getElementById("oidcClientId").value.trim(),
|
||||||
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
oidcClientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||||
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
|
oidcRedirectUri: document.getElementById("oidcRedirectUri").value.trim(),
|
||||||
disableFormLogin: document.getElementById("disableFormLogin").checked,
|
disableFormLogin: document.getElementById("disableFormLogin").checked,
|
||||||
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
|
disableBasicAuth: document.getElementById("disableBasicAuth").checked,
|
||||||
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
|
disableOIDCLogin: document.getElementById("disableOIDCLogin").checked,
|
||||||
enableWebDAV: document.getElementById("enableWebDAV").checked,
|
enableWebDAV: document.getElementById("enableWebDAV").checked,
|
||||||
sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(),
|
sharedMaxUploadSize: document.getElementById("sharedMaxUploadSize").value.trim(),
|
||||||
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
globalOtpauthUrl: document.getElementById("globalOtpauthUrl").value.trim()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function hasUnsavedChanges() {
|
function hasUnsavedChanges() {
|
||||||
@@ -143,18 +143,18 @@ function hasUnsavedChanges() {
|
|||||||
document.getElementById("disableFormLogin").checked !== o.disableFormLogin ||
|
document.getElementById("disableFormLogin").checked !== o.disableFormLogin ||
|
||||||
document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth ||
|
document.getElementById("disableBasicAuth").checked !== o.disableBasicAuth ||
|
||||||
document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin ||
|
document.getElementById("disableOIDCLogin").checked !== o.disableOIDCLogin ||
|
||||||
document.getElementById("enableWebDAV").checked !== o.enableWebDAV ||
|
document.getElementById("enableWebDAV").checked !== o.enableWebDAV ||
|
||||||
document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize ||
|
document.getElementById("sharedMaxUploadSize").value.trim() !== o.sharedMaxUploadSize ||
|
||||||
document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl
|
document.getElementById("globalOtpauthUrl").value.trim() !== o.globalOtpauthUrl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCustomConfirmModal(message) {
|
function showCustomConfirmModal(message) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const modal = document.getElementById("customConfirmModal");
|
const modal = document.getElementById("customConfirmModal");
|
||||||
const msg = document.getElementById("confirmMessage");
|
const msg = document.getElementById("confirmMessage");
|
||||||
const yes = document.getElementById("confirmYesBtn");
|
const yes = document.getElementById("confirmYesBtn");
|
||||||
const no = document.getElementById("confirmNoBtn");
|
const no = document.getElementById("confirmNoBtn");
|
||||||
msg.textContent = message;
|
msg.textContent = message;
|
||||||
modal.style.display = "block";
|
modal.style.display = "block";
|
||||||
function clean() {
|
function clean() {
|
||||||
@@ -163,7 +163,7 @@ function showCustomConfirmModal(message) {
|
|||||||
no.removeEventListener("click", onNo);
|
no.removeEventListener("click", onNo);
|
||||||
}
|
}
|
||||||
function onYes() { clean(); resolve(true); }
|
function onYes() { clean(); resolve(true); }
|
||||||
function onNo() { clean(); resolve(false); }
|
function onNo() { clean(); resolve(false); }
|
||||||
yes.addEventListener("click", onYes);
|
yes.addEventListener("click", onYes);
|
||||||
no.addEventListener("click", onNo);
|
no.addEventListener("click", onNo);
|
||||||
});
|
});
|
||||||
@@ -181,51 +181,57 @@ function toggleSection(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadShareLinksSection() {
|
function loadShareLinksSection() {
|
||||||
const container = document.getElementById("shareLinksContent");
|
const container = document.getElementById("shareLinksContent");
|
||||||
container.textContent = t("loading") + "...";
|
container.textContent = t("loading") + "...";
|
||||||
|
|
||||||
Promise.all([
|
// Helper to fetch a metadata file or return {} on any error
|
||||||
fetch("/api/admin/readMetadata.php?file=share_folder_links.json", { credentials: "include" })
|
const fetchMeta = file =>
|
||||||
.then(r => r.ok ? r.json() : Promise.reject(`Folder fetch ${r.status}`)),
|
fetch(`/api/admin/readMetadata.php?file=${file}`, { credentials: "include" })
|
||||||
fetch("/api/admin/readMetadata.php?file=share_links.json", { credentials: "include" })
|
.then(r => r.ok ? r.json() : {}) // non-2xx → treat as empty
|
||||||
.then(r => r.ok ? r.json() : Promise.reject(`File fetch ${r.status}`))
|
.catch(() => ({}));
|
||||||
])
|
|
||||||
|
Promise.all([
|
||||||
|
fetchMeta("share_folder_links.json"),
|
||||||
|
fetchMeta("share_links.json")
|
||||||
|
])
|
||||||
.then(([folders, files]) => {
|
.then(([folders, files]) => {
|
||||||
|
// If nothing at all, show a friendly message
|
||||||
|
if (Object.keys(folders).length === 0 && Object.keys(files).length === 0) {
|
||||||
|
container.textContent = t("no_shared_links_available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let html = `<h5>${t("folder_shares")}</h5><ul>`;
|
let html = `<h5>${t("folder_shares")}</h5><ul>`;
|
||||||
|
|
||||||
Object.entries(folders).forEach(([token, o]) => {
|
Object.entries(folders).forEach(([token, o]) => {
|
||||||
const lock = o.password ? `🔒 ` : "";
|
const lock = o.password ? `🔒 ` : "";
|
||||||
html += `
|
html += `
|
||||||
<li>
|
<li>
|
||||||
${lock}
|
${lock}<strong>${o.folder}</strong>
|
||||||
<strong>${o.folder}</strong>
|
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
||||||
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
<button type="button"
|
||||||
<button type="button"
|
data-key="${token}"
|
||||||
data-key="${token}"
|
data-type="folder"
|
||||||
data-type="folder"
|
class="btn btn-sm btn-link delete-share">🗑️</button>
|
||||||
class="btn btn-sm btn-link delete-share">🗑️</button>
|
</li>`;
|
||||||
</li>`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `</ul><h5 style="margin-top:1em;">${t("file_shares")}</h5><ul>`;
|
html += `</ul><h5 style="margin-top:1em;">${t("file_shares")}</h5><ul>`;
|
||||||
|
|
||||||
Object.entries(files).forEach(([token, o]) => {
|
Object.entries(files).forEach(([token, o]) => {
|
||||||
const lock = o.password ? `🔒 ` : "";
|
const lock = o.password ? `🔒 ` : "";
|
||||||
html += `
|
html += `
|
||||||
<li>
|
<li>
|
||||||
${lock}
|
${lock}<strong>${o.folder}/${o.file}</strong>
|
||||||
<strong>${o.folder}/${o.file}</strong>
|
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
||||||
<small>(${new Date(o.expires * 1000).toLocaleString()})</small>
|
<button type="button"
|
||||||
<button type="button"
|
data-key="${token}"
|
||||||
data-key="${token}"
|
data-type="file"
|
||||||
data-type="file"
|
class="btn btn-sm btn-link delete-share">🗑️</button>
|
||||||
class="btn btn-sm btn-link delete-share">🗑️</button>
|
</li>`;
|
||||||
</li>`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `</ul>`;
|
html += `</ul>`;
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// wire up delete buttons
|
// wire up delete buttons
|
||||||
container.querySelectorAll(".delete-share").forEach(btn => {
|
container.querySelectorAll(".delete-share").forEach(btn => {
|
||||||
btn.addEventListener("click", evt => {
|
btn.addEventListener("click", evt => {
|
||||||
@@ -235,29 +241,29 @@ function loadShareLinksSection() {
|
|||||||
const endpoint = isFolder
|
const endpoint = isFolder
|
||||||
? "/api/folder/deleteShareFolderLink.php"
|
? "/api/folder/deleteShareFolderLink.php"
|
||||||
: "/api/file/deleteShareLink.php";
|
: "/api/file/deleteShareLink.php";
|
||||||
|
|
||||||
fetch(endpoint, {
|
fetch(endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: new URLSearchParams({ token })
|
body: new URLSearchParams({ token })
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(json => {
|
.then(json => {
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
showToast(t("share_deleted_successfully"));
|
showToast(t("share_deleted_successfully"));
|
||||||
loadShareLinksSection();
|
loadShareLinksSection();
|
||||||
} else {
|
} else {
|
||||||
showToast(t("error_deleting_share") + ": " + (json.error||""), "error");
|
showToast(t("error_deleting_share") + ": " + (json.error || ""), "error");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error("Delete error:", err);
|
console.error("Delete error:", err);
|
||||||
showToast(t("error_deleting_share"), "error");
|
showToast(t("error_deleting_share"), "error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -265,36 +271,36 @@ function loadShareLinksSection() {
|
|||||||
console.error("loadShareLinksSection error:", err);
|
console.error("loadShareLinksSection error:", err);
|
||||||
container.textContent = t("error_loading_share_links");
|
container.textContent = t("error_loading_share_links");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function openAdminPanel() {
|
export function openAdminPanel() {
|
||||||
fetch("/api/admin/getConfig.php",{credentials:"include"})
|
fetch("/api/admin/getConfig.php", { credentials: "include" })
|
||||||
.then(r=>r.json())
|
.then(r => r.json())
|
||||||
.then(config=>{
|
.then(config => {
|
||||||
// apply header title + globals
|
// apply header title + globals
|
||||||
if(config.header_title){
|
if (config.header_title) {
|
||||||
document.querySelector(".header-title h1").textContent = config.header_title;
|
document.querySelector(".header-title h1").textContent = config.header_title;
|
||||||
window.headerTitle = config.header_title;
|
window.headerTitle = config.header_title;
|
||||||
}
|
}
|
||||||
if(config.oidc) Object.assign(window.currentOIDCConfig,config.oidc);
|
if (config.oidc) Object.assign(window.currentOIDCConfig, config.oidc);
|
||||||
if(config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
if (config.globalOtpauthUrl) window.currentOIDCConfig.globalOtpauthUrl = config.globalOtpauthUrl;
|
||||||
|
|
||||||
const dark = document.body.classList.contains("dark-mode");
|
const dark = document.body.classList.contains("dark-mode");
|
||||||
const bg = dark?"rgba(0,0,0,0.7)":"rgba(0,0,0,0.3)";
|
const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
const inner= `
|
const inner = `
|
||||||
background:${dark?"#2c2c2c":"#fff"};
|
background:${dark ? "#2c2c2c" : "#fff"};
|
||||||
color:${dark?"#e0e0e0":"#000"};
|
color:${dark ? "#e0e0e0" : "#000"};
|
||||||
padding:20px; max-width:1100px; width:50%;
|
padding:20px; max-width:1100px; width:50%;
|
||||||
border-radius:8px; position:relative;
|
border-radius:8px; position:relative;
|
||||||
max-height:90vh; overflow:auto;
|
max-height:90vh; overflow:auto;
|
||||||
border:1px solid ${dark?"#555":"#ccc"};
|
border:1px solid ${dark ? "#555" : "#ccc"};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let mdl = document.getElementById("adminPanelModal");
|
let mdl = document.getElementById("adminPanelModal");
|
||||||
if(!mdl){
|
if (!mdl) {
|
||||||
mdl = document.createElement("div");
|
mdl = document.createElement("div");
|
||||||
mdl.id="adminPanelModal";
|
mdl.id = "adminPanelModal";
|
||||||
mdl.style.cssText = `
|
mdl.style.cssText = `
|
||||||
position:fixed; top:0; left:0;
|
position:fixed; top:0; left:0;
|
||||||
width:100vw; height:100vh;
|
width:100vw; height:100vh;
|
||||||
@@ -310,14 +316,14 @@ export function openAdminPanel() {
|
|||||||
|
|
||||||
<!-- each section: header + content -->
|
<!-- each section: header + content -->
|
||||||
${[
|
${[
|
||||||
{ id:"userManagement", label:t("user_management") },
|
{ id: "userManagement", label: t("user_management") },
|
||||||
{ id:"headerSettings", label:t("header_settings") },
|
{ id: "headerSettings", label: t("header_settings") },
|
||||||
{ id:"loginOptions", label:t("login_options") },
|
{ id: "loginOptions", label: t("login_options") },
|
||||||
{ id:"webdav", label:"WebDAV Access" },
|
{ id: "webdav", label: "WebDAV Access" },
|
||||||
{ id:"upload", label:t("shared_max_upload_size_bytes") },
|
{ id: "upload", label: t("shared_max_upload_size_bytes_title") },
|
||||||
{ id:"oidc", label:t("oidc_configuration") + " & TOTP" },
|
{ id: "oidc", label: t("oidc_configuration") + " & TOTP" },
|
||||||
{ id:"shareLinks", label:t("manage_shared_links") }
|
{ id: "shareLinks", label: t("manage_shared_links") }
|
||||||
].map(sec=>`
|
].map(sec => `
|
||||||
<div id="${sec.id}Header" class="section-header collapsed">
|
<div id="${sec.id}Header" class="section-header collapsed">
|
||||||
${sec.label} <i class="material-icons">expand_more</i>
|
${sec.label} <i class="material-icons">expand_more</i>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,10 +346,10 @@ export function openAdminPanel() {
|
|||||||
.addEventListener("click", closeAdminPanel);
|
.addEventListener("click", closeAdminPanel);
|
||||||
|
|
||||||
// Section toggles
|
// Section toggles
|
||||||
["userManagement","headerSettings","loginOptions","webdav","upload","oidc","shareLinks"]
|
["userManagement", "headerSettings", "loginOptions", "webdav", "upload", "oidc", "shareLinks"]
|
||||||
.forEach(id=>{
|
.forEach(id => {
|
||||||
document.getElementById(id+"Header")
|
document.getElementById(id + "Header")
|
||||||
.addEventListener("click", ()=>toggleSection(id));
|
.addEventListener("click", () => toggleSection(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate each section’s CONTENT:
|
// Populate each section’s CONTENT:
|
||||||
@@ -354,14 +360,14 @@ export function openAdminPanel() {
|
|||||||
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
<button type="button" id="adminOpenUserPermissions" class="btn btn-secondary">${t("user_permissions")}</button>
|
||||||
`;
|
`;
|
||||||
document.getElementById("adminOpenAddUser")
|
document.getElementById("adminOpenAddUser")
|
||||||
.addEventListener("click",()=>{
|
.addEventListener("click", () => {
|
||||||
toggleVisibility("addUserModal",true);
|
toggleVisibility("addUserModal", true);
|
||||||
document.getElementById("newUsername").focus();
|
document.getElementById("newUsername").focus();
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenRemoveUser")
|
document.getElementById("adminOpenRemoveUser")
|
||||||
.addEventListener("click",()=>{
|
.addEventListener("click", () => {
|
||||||
if(typeof window.loadUserList==="function") window.loadUserList();
|
if (typeof window.loadUserList === "function") window.loadUserList();
|
||||||
toggleVisibility("removeUserModal",true);
|
toggleVisibility("removeUserModal", true);
|
||||||
});
|
});
|
||||||
document.getElementById("adminOpenUserPermissions")
|
document.getElementById("adminOpenUserPermissions")
|
||||||
.addEventListener("click", openUserPermissionsModal);
|
.addEventListener("click", openUserPermissionsModal);
|
||||||
@@ -401,21 +407,21 @@ export function openAdminPanel() {
|
|||||||
<div class="form-group"><label for="oidcClientId">${t("oidc_client_id")}:</label><input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" /></div>
|
<div class="form-group"><label for="oidcClientId">${t("oidc_client_id")}:</label><input type="text" id="oidcClientId" class="form-control" value="${window.currentOIDCConfig.clientId}" /></div>
|
||||||
<div class="form-group"><label for="oidcClientSecret">${t("oidc_client_secret")}:</label><input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" /></div>
|
<div class="form-group"><label for="oidcClientSecret">${t("oidc_client_secret")}:</label><input type="text" id="oidcClientSecret" class="form-control" value="${window.currentOIDCConfig.clientSecret}" /></div>
|
||||||
<div class="form-group"><label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label><input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" /></div>
|
<div class="form-group"><label for="oidcRedirectUri">${t("oidc_redirect_uri")}:</label><input type="text" id="oidcRedirectUri" class="form-control" value="${window.currentOIDCConfig.redirectUri}" /></div>
|
||||||
<div class="form-group"><label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label><input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl||'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" /></div>
|
<div class="form-group"><label for="globalOtpauthUrl">${t("global_otpauth_url")}:</label><input type="text" id="globalOtpauthUrl" class="form-control" value="${window.currentOIDCConfig.globalOtpauthUrl || 'otpauth://totp/{label}?secret={secret}&issuer=FileRise'}" /></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// — Share Links —
|
// — Share Links —
|
||||||
document.getElementById("shareLinksContent").textContent = t("loading")+"…";
|
document.getElementById("shareLinksContent").textContent = t("loading") + "…";
|
||||||
|
|
||||||
// — Save handler & constraints —
|
// — Save handler & constraints —
|
||||||
document.getElementById("saveAdminSettings")
|
document.getElementById("saveAdminSettings")
|
||||||
.addEventListener("click", handleSave);
|
.addEventListener("click", handleSave);
|
||||||
["disableFormLogin","disableBasicAuth","disableOIDCLogin"].forEach(id=>{
|
["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"].forEach(id => {
|
||||||
document.getElementById(id)
|
document.getElementById(id)
|
||||||
.addEventListener("change", e=>{
|
.addEventListener("change", e => {
|
||||||
const chk = ["disableFormLogin","disableBasicAuth","disableOIDCLogin"]
|
const chk = ["disableFormLogin", "disableBasicAuth", "disableOIDCLogin"]
|
||||||
.filter(i=>document.getElementById(i).checked).length;
|
.filter(i => document.getElementById(i).checked).length;
|
||||||
if(chk===3){
|
if (chk === 3) {
|
||||||
showToast(t("at_least_one_login_method"));
|
showToast(t("at_least_one_login_method"));
|
||||||
e.target.checked = false;
|
e.target.checked = false;
|
||||||
}
|
}
|
||||||
@@ -423,85 +429,85 @@ export function openAdminPanel() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize inputs from config + capture
|
// Initialize inputs from config + capture
|
||||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin===true;
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth===true;
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin===true;
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
document.getElementById("enableWebDAV").checked = config.enableWebDAV===true;
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize||"";
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// modal already exists → just refresh values & re-show
|
// modal already exists → just refresh values & re-show
|
||||||
mdl.style.display = "flex";
|
mdl.style.display = "flex";
|
||||||
// update dark/light as above...
|
// update dark/light as above...
|
||||||
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin===true;
|
document.getElementById("disableFormLogin").checked = config.loginOptions.disableFormLogin === true;
|
||||||
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth===true;
|
document.getElementById("disableBasicAuth").checked = config.loginOptions.disableBasicAuth === true;
|
||||||
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin===true;
|
document.getElementById("disableOIDCLogin").checked = config.loginOptions.disableOIDCLogin === true;
|
||||||
document.getElementById("enableWebDAV").checked = config.enableWebDAV===true;
|
document.getElementById("enableWebDAV").checked = config.enableWebDAV === true;
|
||||||
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize||"";
|
document.getElementById("sharedMaxUploadSize").value = config.sharedMaxUploadSize || "";
|
||||||
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
document.getElementById("oidcProviderUrl").value = window.currentOIDCConfig.providerUrl;
|
||||||
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
document.getElementById("oidcClientId").value = window.currentOIDCConfig.clientId;
|
||||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig.clientSecret;
|
||||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig.redirectUri;
|
||||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl||'';
|
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig.globalOtpauthUrl || '';
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(()=>{/* if even fetching fails, open empty panel */});
|
.catch(() => {/* if even fetching fails, open empty panel */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave(){
|
function handleSave() {
|
||||||
const dFL = document.getElementById("disableFormLogin").checked;
|
const dFL = document.getElementById("disableFormLogin").checked;
|
||||||
const dBA = document.getElementById("disableBasicAuth").checked;
|
const dBA = document.getElementById("disableBasicAuth").checked;
|
||||||
const dOIDC = document.getElementById("disableOIDCLogin").checked;
|
const dOIDC = document.getElementById("disableOIDCLogin").checked;
|
||||||
const eWD = document.getElementById("enableWebDAV").checked;
|
const eWD = document.getElementById("enableWebDAV").checked;
|
||||||
const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value,10)||0;
|
const sMax = parseInt(document.getElementById("sharedMaxUploadSize").value, 10) || 0;
|
||||||
const nHT = document.getElementById("headerTitle").value.trim();
|
const nHT = document.getElementById("headerTitle").value.trim();
|
||||||
const nOIDC = {
|
const nOIDC = {
|
||||||
providerUrl : document.getElementById("oidcProviderUrl").value.trim(),
|
providerUrl: document.getElementById("oidcProviderUrl").value.trim(),
|
||||||
clientId : document.getElementById("oidcClientId").value.trim(),
|
clientId: document.getElementById("oidcClientId").value.trim(),
|
||||||
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
clientSecret: document.getElementById("oidcClientSecret").value.trim(),
|
||||||
redirectUri : document.getElementById("oidcRedirectUri").value.trim()
|
redirectUri: document.getElementById("oidcRedirectUri").value.trim()
|
||||||
};
|
};
|
||||||
const gURL = document.getElementById("globalOtpauthUrl").value.trim();
|
const gURL = document.getElementById("globalOtpauthUrl").value.trim();
|
||||||
|
|
||||||
if([dFL,dBA,dOIDC].filter(x=>x).length===3){
|
if ([dFL, dBA, dOIDC].filter(x => x).length === 3) {
|
||||||
showToast(t("at_least_one_login_method"));
|
showToast(t("at_least_one_login_method"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRequest("/api/admin/updateConfig.php","POST",{
|
sendRequest("/api/admin/updateConfig.php", "POST", {
|
||||||
header_title:nHT, oidc:nOIDC,
|
header_title: nHT, oidc: nOIDC,
|
||||||
disableFormLogin:dFL, disableBasicAuth:dBA, disableOIDCLogin:dOIDC,
|
disableFormLogin: dFL, disableBasicAuth: dBA, disableOIDCLogin: dOIDC,
|
||||||
enableWebDAV:eWD, sharedMaxUploadSize:sMax, globalOtpauthUrl:gURL
|
enableWebDAV: eWD, sharedMaxUploadSize: sMax, globalOtpauthUrl: gURL
|
||||||
},{
|
}, {
|
||||||
"X-CSRF-Token":window.csrfToken
|
"X-CSRF-Token": window.csrfToken
|
||||||
}).then(res=>{
|
}).then(res => {
|
||||||
if(res.success){
|
if (res.success) {
|
||||||
showToast(t("settings_updated_successfully"),"success");
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
captureInitialAdminConfig();
|
captureInitialAdminConfig();
|
||||||
closeAdminPanel();
|
closeAdminPanel();
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
} else {
|
} else {
|
||||||
showToast(t("error_updating_settings")+": "+(res.error||t("unknown_error")),"error");
|
showToast(t("error_updating_settings") + ": " + (res.error || t("unknown_error")), "error");
|
||||||
}
|
}
|
||||||
}).catch(()=>{/*noop*/});
|
}).catch(() => {/*noop*/ });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeAdminPanel() {
|
export async function closeAdminPanel() {
|
||||||
if(hasUnsavedChanges()){
|
if (hasUnsavedChanges()) {
|
||||||
const ok = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
const ok = await showCustomConfirmModal(t("unsaved_changes_confirm"));
|
||||||
if(!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
document.getElementById("adminPanelModal").style.display="none";
|
document.getElementById("adminPanelModal").style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- New: User Permissions Modal ---
|
// --- New: User Permissions Modal ---
|
||||||
export function openUserPermissionsModal() {
|
export function openUserPermissionsModal() {
|
||||||
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
let userPermissionsModal = document.getElementById("userPermissionsModal");
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
const modalContentStyles = `
|
const modalContentStyles = `
|
||||||
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
background: ${isDarkMode ? "#2c2c2c" : "#fff"};
|
||||||
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
color: ${isDarkMode ? "#e0e0e0" : "#000"};
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -510,11 +516,11 @@ export function openUserPermissionsModal() {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!userPermissionsModal) {
|
if (!userPermissionsModal) {
|
||||||
userPermissionsModal = document.createElement("div");
|
userPermissionsModal = document.createElement("div");
|
||||||
userPermissionsModal.id = "userPermissionsModal";
|
userPermissionsModal.id = "userPermissionsModal";
|
||||||
userPermissionsModal.style.cssText = `
|
userPermissionsModal.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -526,7 +532,7 @@ export function openUserPermissionsModal() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 3500;
|
z-index: 3500;
|
||||||
`;
|
`;
|
||||||
userPermissionsModal.innerHTML = `
|
userPermissionsModal.innerHTML = `
|
||||||
<div class="modal-content" style="${modalContentStyles}">
|
<div class="modal-content" style="${modalContentStyles}">
|
||||||
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
<span id="closeUserPermissionsModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">×</span>
|
||||||
<h3>${t("user_permissions")}</h3>
|
<h3>${t("user_permissions")}</h3>
|
||||||
@@ -539,92 +545,92 @@ export function openUserPermissionsModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(userPermissionsModal);
|
document.body.appendChild(userPermissionsModal);
|
||||||
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
document.getElementById("closeUserPermissionsModal").addEventListener("click", () => {
|
||||||
userPermissionsModal.style.display = "none";
|
userPermissionsModal.style.display = "none";
|
||||||
});
|
});
|
||||||
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
document.getElementById("cancelUserPermissionsBtn").addEventListener("click", () => {
|
||||||
userPermissionsModal.style.display = "none";
|
userPermissionsModal.style.display = "none";
|
||||||
});
|
});
|
||||||
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
document.getElementById("saveUserPermissionsBtn").addEventListener("click", () => {
|
||||||
// Collect permissions data from each user row.
|
// Collect permissions data from each user row.
|
||||||
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
const rows = userPermissionsModal.querySelectorAll(".user-permission-row");
|
||||||
const permissionsData = [];
|
const permissionsData = [];
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const username = row.getAttribute("data-username");
|
const username = row.getAttribute("data-username");
|
||||||
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
const folderOnlyCheckbox = row.querySelector("input[data-permission='folderOnly']");
|
||||||
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
const readOnlyCheckbox = row.querySelector("input[data-permission='readOnly']");
|
||||||
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
const disableUploadCheckbox = row.querySelector("input[data-permission='disableUpload']");
|
||||||
permissionsData.push({
|
permissionsData.push({
|
||||||
username,
|
username,
|
||||||
folderOnly: folderOnlyCheckbox.checked,
|
folderOnly: folderOnlyCheckbox.checked,
|
||||||
readOnly: readOnlyCheckbox.checked,
|
readOnly: readOnlyCheckbox.checked,
|
||||||
disableUpload: disableUploadCheckbox.checked
|
disableUpload: disableUploadCheckbox.checked
|
||||||
});
|
|
||||||
});
|
});
|
||||||
// Send the permissionsData to the server.
|
|
||||||
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
|
||||||
.then(response => {
|
|
||||||
if (response.success) {
|
|
||||||
showToast(t("user_permissions_updated_successfully"));
|
|
||||||
userPermissionsModal.style.display = "none";
|
|
||||||
} else {
|
|
||||||
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
showToast(t("error_updating_permissions"));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
// Send the permissionsData to the server.
|
||||||
userPermissionsModal.style.display = "flex";
|
sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken })
|
||||||
}
|
.then(response => {
|
||||||
// Load the list of users into the modal.
|
if (response.success) {
|
||||||
loadUserPermissionsList();
|
showToast(t("user_permissions_updated_successfully"));
|
||||||
|
userPermissionsModal.style.display = "none";
|
||||||
|
} else {
|
||||||
|
showToast(t("error_updating_permissions") + ": " + (response.error || t("unknown_error")));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast(t("error_updating_permissions"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userPermissionsModal.style.display = "flex";
|
||||||
}
|
}
|
||||||
|
// Load the list of users into the modal.
|
||||||
function loadUserPermissionsList() {
|
loadUserPermissionsList();
|
||||||
const listContainer = document.getElementById("userPermissionsList");
|
}
|
||||||
if (!listContainer) return;
|
|
||||||
listContainer.innerHTML = "";
|
function loadUserPermissionsList() {
|
||||||
|
const listContainer = document.getElementById("userPermissionsList");
|
||||||
// First, fetch the current permissions from the server.
|
if (!listContainer) return;
|
||||||
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
listContainer.innerHTML = "";
|
||||||
.then(response => response.json())
|
|
||||||
.then(permissionsData => {
|
// First, fetch the current permissions from the server.
|
||||||
// Then, fetch the list of users.
|
fetch("/api/getUserPermissions.php", { credentials: "include" })
|
||||||
return fetch("/api/getUsers.php", { credentials: "include" })
|
.then(response => response.json())
|
||||||
.then(response => response.json())
|
.then(permissionsData => {
|
||||||
.then(usersData => {
|
// Then, fetch the list of users.
|
||||||
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
return fetch("/api/getUsers.php", { credentials: "include" })
|
||||||
if (users.length === 0) {
|
.then(response => response.json())
|
||||||
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
.then(usersData => {
|
||||||
return;
|
const users = Array.isArray(usersData) ? usersData : (usersData.users || []);
|
||||||
}
|
if (users.length === 0) {
|
||||||
users.forEach(user => {
|
listContainer.innerHTML = "<p>" + t("no_users_found") + "</p>";
|
||||||
// Skip admin users.
|
return;
|
||||||
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
}
|
||||||
|
users.forEach(user => {
|
||||||
// Use stored permissions if available; otherwise fall back to defaults.
|
// Skip admin users.
|
||||||
const defaultPerm = {
|
if ((user.role && user.role === "1") || user.username.toLowerCase() === "admin") return;
|
||||||
folderOnly: false,
|
|
||||||
readOnly: false,
|
// Use stored permissions if available; otherwise fall back to defaults.
|
||||||
disableUpload: false,
|
const defaultPerm = {
|
||||||
};
|
folderOnly: false,
|
||||||
|
readOnly: false,
|
||||||
// Normalize the username key to match server storage (e.g., lowercase)
|
disableUpload: false,
|
||||||
const usernameKey = user.username.toLowerCase();
|
};
|
||||||
|
|
||||||
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
// Normalize the username key to match server storage (e.g., lowercase)
|
||||||
? permissionsData[usernameKey]
|
const usernameKey = user.username.toLowerCase();
|
||||||
: defaultPerm;
|
|
||||||
|
const userPerm = (permissionsData && typeof permissionsData === "object" && (usernameKey in permissionsData))
|
||||||
// Create a row for the user.
|
? permissionsData[usernameKey]
|
||||||
const row = document.createElement("div");
|
: defaultPerm;
|
||||||
row.classList.add("user-permission-row");
|
|
||||||
row.setAttribute("data-username", user.username);
|
// Create a row for the user.
|
||||||
row.style.padding = "10px 0";
|
const row = document.createElement("div");
|
||||||
row.innerHTML = `
|
row.classList.add("user-permission-row");
|
||||||
|
row.setAttribute("data-username", user.username);
|
||||||
|
row.style.padding = "10px 0";
|
||||||
|
row.innerHTML = `
|
||||||
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
<div style="font-weight: bold; margin-bottom: 5px;">${user.username}</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||||
<label style="display: flex; align-items: center; gap: 5px;">
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
@@ -642,11 +648,11 @@ export function openUserPermissionsModal() {
|
|||||||
</div>
|
</div>
|
||||||
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
<hr style="margin-top: 10px; border: 0; border-bottom: 1px solid #ccc;">
|
||||||
`;
|
`;
|
||||||
listContainer.appendChild(row);
|
listContainer.appendChild(row);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.catch(() => {
|
})
|
||||||
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
.catch(() => {
|
||||||
});
|
listContainer.innerHTML = "<p>" + t("error_loading_users") + "</p>";
|
||||||
}
|
});
|
||||||
|
}
|
||||||
@@ -184,6 +184,7 @@ const translations = {
|
|||||||
|
|
||||||
// Admin Panel
|
// Admin Panel
|
||||||
"header_settings": "Header Settings",
|
"header_settings": "Header Settings",
|
||||||
|
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
|
||||||
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
||||||
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
||||||
"manage_shared_links": "Manage Shared Links",
|
"manage_shared_links": "Manage Shared Links",
|
||||||
@@ -194,6 +195,7 @@ const translations = {
|
|||||||
"share_deleted_successfully": "Share deleted successfully",
|
"share_deleted_successfully": "Share deleted successfully",
|
||||||
"error_deleting_share": "Error deleting share",
|
"error_deleting_share": "Error deleting share",
|
||||||
"password_protected": "Password protected",
|
"password_protected": "Password protected",
|
||||||
|
"no_shared_links_available": "No shared links available",
|
||||||
|
|
||||||
|
|
||||||
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
// NEW KEYS ADDED FOR ADMIN, USER PANELS, AND TOTP MODALS:
|
||||||
|
|||||||
Reference in New Issue
Block a user