feat(admin): add box preview and password bypass for administrators
Introduce an `AdminViewBox` handler and route that allows administrators to view any box directly. If the box is password-protected, the handler bypasses the protection by setting an unlock cookie with an unlock token and logs the bypass event. Additionally, add CSS and JS foundations for a file context menu and preview actions in the file browser UI.
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
const fileBrowser = document.querySelector("[data-file-browser]");
|
||||
const viewButtons = document.querySelectorAll("[data-view-button]");
|
||||
const previewImages = document.querySelector("[data-preview-images]");
|
||||
const previewActions = document.querySelectorAll("[data-preview-action]");
|
||||
const fileContextMenu = document.querySelector("[data-file-context-menu]");
|
||||
let ctrlCopyMode = false;
|
||||
let contextFile = null;
|
||||
|
||||
if (fileBrowser) {
|
||||
viewButtons.forEach((button) => {
|
||||
@@ -34,6 +38,80 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (fileBrowser && fileContextMenu) {
|
||||
fileBrowser.addEventListener("contextmenu", (event) => {
|
||||
const card = event.target.closest("[data-file-context]");
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
contextFile = {
|
||||
previewURL: card.dataset.previewUrl,
|
||||
viewURL: card.dataset.viewUrl,
|
||||
downloadURL: card.dataset.downloadUrl,
|
||||
fileName: card.dataset.fileName,
|
||||
};
|
||||
showContextMenu(event.clientX, event.clientY);
|
||||
});
|
||||
|
||||
fileContextMenu.addEventListener("click", async (event) => {
|
||||
const button = event.target.closest("[data-context-action]");
|
||||
if (!button || !contextFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldHide = await runContextAction(button.dataset.contextAction, contextFile);
|
||||
if (shouldHide !== false) {
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!fileContextMenu.contains(event.target)) {
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", hideContextMenu);
|
||||
window.addEventListener("scroll", hideContextMenu, true);
|
||||
}
|
||||
|
||||
if (previewActions.length > 0) {
|
||||
previewActions.forEach((button) => {
|
||||
button.addEventListener("click", async (event) => {
|
||||
if (!event.ctrlKey && !ctrlCopyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
await copyPreviewLink(button);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Control") {
|
||||
setPreviewCopyMode(true);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", (event) => {
|
||||
if (event.key === "Control") {
|
||||
setPreviewCopyMode(false);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("blur", () => {
|
||||
setPreviewCopyMode(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!form || !dropZone || !fileInput) {
|
||||
return;
|
||||
}
|
||||
@@ -267,7 +345,7 @@
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(text);
|
||||
await writeClipboard(text);
|
||||
const previous = button.textContent;
|
||||
button.textContent = copiedLabel;
|
||||
setTimeout(() => {
|
||||
@@ -275,6 +353,102 @@
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
async function copyPreviewLink(button) {
|
||||
await writeClipboard(button.href);
|
||||
const label = button.querySelector("[data-preview-label]");
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
|
||||
label.textContent = "Copied";
|
||||
setTimeout(() => {
|
||||
label.textContent = ctrlCopyMode ? button.dataset.copyLabel || "Copy link" : button.dataset.viewLabel || "View";
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function setPreviewCopyMode(enabled) {
|
||||
ctrlCopyMode = enabled;
|
||||
previewActions.forEach((button) => {
|
||||
const label = button.querySelector("[data-preview-label]");
|
||||
const viewIcon = button.querySelector("[data-preview-view-icon]");
|
||||
const copyIcon = button.querySelector("[data-preview-copy-icon]");
|
||||
if (label) {
|
||||
label.textContent = enabled ? button.dataset.copyLabel || "Copy link" : button.dataset.viewLabel || "View";
|
||||
}
|
||||
if (viewIcon) {
|
||||
viewIcon.hidden = enabled;
|
||||
}
|
||||
if (copyIcon) {
|
||||
copyIcon.hidden = !enabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function runContextAction(action, file) {
|
||||
if (action === "preview") {
|
||||
openInNewTab(file.previewURL);
|
||||
return true;
|
||||
}
|
||||
if (action === "view") {
|
||||
openInNewTab(file.viewURL);
|
||||
return true;
|
||||
}
|
||||
if (action === "copy-preview") {
|
||||
await writeClipboard(file.previewURL);
|
||||
return true;
|
||||
}
|
||||
if (action === "copy-download") {
|
||||
await writeClipboard(file.downloadURL);
|
||||
return true;
|
||||
}
|
||||
if (action === "download") {
|
||||
openInNewTab(file.downloadURL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function showContextMenu(x, y) {
|
||||
fileContextMenu.hidden = false;
|
||||
fileContextMenu.style.left = "0px";
|
||||
fileContextMenu.style.top = "0px";
|
||||
|
||||
const rect = fileContextMenu.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const left = Math.min(x, window.innerWidth - rect.width - margin);
|
||||
const top = Math.min(y, window.innerHeight - rect.height - margin);
|
||||
fileContextMenu.style.left = `${Math.max(margin, left)}px`;
|
||||
fileContextMenu.style.top = `${Math.max(margin, top)}px`;
|
||||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
if (!fileContextMenu || fileContextMenu.hidden) {
|
||||
return;
|
||||
}
|
||||
fileContextMenu.hidden = true;
|
||||
contextFile = null;
|
||||
}
|
||||
|
||||
function openInNewTab(url) {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
async function writeClipboard(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.append(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
textarea.remove();
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
|
||||
Reference in New Issue
Block a user