diff --git a/Overlord-Server/public/assets/file-share.js b/Overlord-Server/public/assets/file-share.js new file mode 100644 index 0000000..b9948b2 --- /dev/null +++ b/Overlord-Server/public/assets/file-share.js @@ -0,0 +1,389 @@ +const fileList = document.getElementById("file-list"); +const uploadSection = document.getElementById("upload-section"); +const uploadForm = document.getElementById("upload-form"); +const uploadInfinite = document.getElementById("upload-infinite"); +const uploadMaxDownloads = document.getElementById("upload-max-downloads"); + +let currentUser = null; + +function formatBytes(bytes) { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} + +function timeAgo(ts) { + const seconds = Math.floor((Date.now() - ts) / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return new Date(ts).toLocaleDateString(); +} + +function escapeHtml(str) { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +function getDownloadUrl(fileId) { + return `${window.location.origin}/api/file-share/${fileId}/download`; +} + +uploadInfinite?.addEventListener("change", () => { + if (uploadInfinite.checked) { + uploadMaxDownloads.value = ""; + uploadMaxDownloads.disabled = true; + } else { + uploadMaxDownloads.disabled = false; + uploadMaxDownloads.focus(); + } +}); +if (uploadMaxDownloads) uploadMaxDownloads.disabled = true; + +const passwordInfoBtn = document.getElementById("password-info-btn"); +const passwordInfoPanel = document.getElementById("password-info-panel"); +passwordInfoBtn?.addEventListener("click", () => { + passwordInfoPanel?.classList.toggle("hidden"); +}); + +async function loadCurrentUser() { + try { + const res = await fetch("/api/auth/me"); + if (!res.ok) { + window.location.href = "/"; + return; + } + currentUser = await res.json(); + if (currentUser.role === "viewer") { + window.location.href = "/"; + return; + } + } catch { + window.location.href = "/"; + } +} + +async function loadFiles() { + try { + const res = await fetch("/api/file-share"); + if (!res.ok) { + fileList.innerHTML = `Failed to load files`; + return; + } + const data = await res.json(); + + if (data.canUpload && uploadSection) { + uploadSection.classList.remove("hidden"); + } + + if (!data.files || data.files.length === 0) { + fileList.innerHTML = ` + No files shared yet + `; + return; + } + + fileList.innerHTML = data.files + .map((f) => { + const isOwner = currentUser && f.uploadedBy === currentUser.userId; + const isAdmin = currentUser && currentUser.role === "admin"; + const canManage = isOwner || isAdmin; + + const expired = f.expiresAt && f.expiresAt < Date.now(); + const limitReached = f.maxDownloads !== null && f.downloadCount >= f.maxDownloads; + + let statusBadges = ""; + if (f.hasPassword) { + statusBadges += `Password `; + } + if (expired) { + statusBadges += `Expired `; + } else if (limitReached) { + statusBadges += `Limit Reached `; + } else if (f.expiresAt) { + statusBadges += `Timed `; + } + if (!statusBadges) { + statusBadges = `Active`; + } + + const downloadStr = f.maxDownloads !== null + ? `${f.downloadCount} / ${f.maxDownloads}` + : `${f.downloadCount} / ∞`; + + return ` + + +
+ +
+ ${escapeHtml(f.filename)} + ${f.description ? `${escapeHtml(f.description)}` : ""} +
+
+ + ${formatBytes(f.size)} + ${escapeHtml(f.uploadedByUsername)} + ${downloadStr} + ${statusBadges} + ${timeAgo(f.createdAt)} + +
+ + ${canManage ? ` + + + ` : ""} +
+ + `; + }) + .join(""); + + attachActionListeners(); + } catch (err) { + console.error("Load files error:", err); + fileList.innerHTML = `Failed to load files`; + } +} + +function attachActionListeners() { + fileList.addEventListener("click", (e) => { + const btn = e.target.closest(".action-btn"); + if (!btn) return; + + const action = btn.dataset.action; + const fileId = btn.dataset.fileId; + + switch (action) { + case "copy-link": + copyDownloadLink(fileId); + break; + case "edit": + openEditModal(btn); + break; + case "delete": + deleteFile(fileId, btn.dataset.filename); + break; + } + }); +} + +function copyDownloadLink(fileId) { + const url = getDownloadUrl(fileId); + navigator.clipboard.writeText(url).then(() => { + if (window.showToast) { + window.showToast("Download link copied to clipboard!", "success"); + } + }).catch(() => { + const input = document.createElement("input"); + input.value = url; + document.body.appendChild(input); + input.select(); + document.execCommand("copy"); + document.body.removeChild(input); + if (window.showToast) { + window.showToast("Download link copied!", "success"); + } + }); +} + +async function deleteFile(fileId, filename) { + if (!confirm(`Delete "${filename}"? This cannot be undone.`)) return; + + try { + const res = await fetch(`/api/file-share/${fileId}`, { method: "DELETE" }); + if (res.ok) { + if (window.showToast) window.showToast("File deleted", "success"); + loadFiles(); + } else { + const data = await res.json().catch(() => ({})); + alert(data.error || "Failed to delete file"); + } + } catch (err) { + console.error("Delete error:", err); + alert("Network error. Please try again."); + } +} + +const editModal = document.getElementById("edit-modal"); +const editFileId = document.getElementById("edit-file-id"); +const editPassword = document.getElementById("edit-password"); +const editMaxDownloads = document.getElementById("edit-max-downloads"); +const editInfinite = document.getElementById("edit-infinite"); +const editExpiry = document.getElementById("edit-expiry"); +const editDescription = document.getElementById("edit-description"); + +function openEditModal(btn) { + editFileId.value = btn.dataset.fileId; + editPassword.value = ""; + editDescription.value = btn.dataset.description || ""; + + const maxDl = btn.dataset.maxDownloads; + if (maxDl === "" || maxDl === "null") { + editInfinite.checked = true; + editMaxDownloads.value = ""; + editMaxDownloads.disabled = true; + } else { + editInfinite.checked = false; + editMaxDownloads.value = maxDl; + editMaxDownloads.disabled = false; + } + + const expiresAt = btn.dataset.expiresAt; + if (expiresAt && expiresAt !== "null" && expiresAt !== "") { + const d = new Date(parseInt(expiresAt)); + editExpiry.value = d.toISOString().slice(0, 16); + } else { + editExpiry.value = ""; + } + + editModal.classList.remove("hidden"); +} + +editInfinite?.addEventListener("change", () => { + if (editInfinite.checked) { + editMaxDownloads.value = ""; + editMaxDownloads.disabled = true; + } else { + editMaxDownloads.disabled = false; + editMaxDownloads.focus(); + } +}); + +document.getElementById("edit-cancel")?.addEventListener("click", () => { + editModal.classList.add("hidden"); +}); + +editModal?.addEventListener("click", (e) => { + if (e.target === editModal) editModal.classList.add("hidden"); +}); + +document.getElementById("edit-save")?.addEventListener("click", async () => { + const fileId = editFileId.value; + const body = {}; + + const pw = editPassword.value; + if (pw !== "") { + body.password = pw; + } + + if (editInfinite.checked) { + body.maxDownloads = null; + } else { + const val = parseInt(editMaxDownloads.value, 10); + if (!isNaN(val) && val > 0) { + body.maxDownloads = val; + } + } + + if (editExpiry.value) { + body.expiresAt = new Date(editExpiry.value).getTime(); + } else { + body.expiresAt = null; + } + + body.description = editDescription.value || null; + + try { + const res = await fetch(`/api/file-share/${fileId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (res.ok) { + editModal.classList.add("hidden"); + if (window.showToast) window.showToast("Settings updated", "success"); + loadFiles(); + } else { + const data = await res.json().catch(() => ({})); + alert(data.error || "Failed to update"); + } + } catch (err) { + console.error("Edit error:", err); + alert("Network error. Please try again."); + } +}); + +uploadForm?.addEventListener("submit", async (e) => { + e.preventDefault(); + + const fileInput = document.getElementById("upload-file"); + const file = fileInput.files[0]; + if (!file) return; + + const btn = document.getElementById("upload-btn"); + btn.disabled = true; + btn.innerHTML = ' Uploading...'; + + const formData = new FormData(); + formData.append("file", file); + + const password = document.getElementById("upload-password").value; + if (password) formData.append("password", password); + + if (!uploadInfinite.checked) { + const maxDl = uploadMaxDownloads.value; + if (maxDl) formData.append("maxDownloads", maxDl); + } + + const expiry = document.getElementById("upload-expiry").value; + if (expiry) { + formData.append("expiresAt", String(new Date(expiry).getTime())); + } + + const description = document.getElementById("upload-description").value; + if (description) formData.append("description", description); + + try { + const res = await fetch("/api/file-share/upload", { + method: "POST", + body: formData, + }); + + const data = await res.json(); + + if (res.ok) { + if (window.showToast) window.showToast(`"${data.filename}" uploaded successfully!`, "success"); + uploadForm.reset(); + uploadInfinite.checked = true; + uploadMaxDownloads.disabled = true; + loadFiles(); + } else { + alert(data.error || "Upload failed"); + } + } catch (err) { + console.error("Upload error:", err); + alert("Network error. Please try again."); + } finally { + btn.disabled = false; + btn.innerHTML = ' Upload'; + } +}); + +(async () => { + await loadCurrentUser(); + await loadFiles(); +})(); diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index f43ed4c..ecce22d 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -30,6 +30,7 @@ if (host) { "/users": "users-link", "/user-client-access": "users-link", "/notifications": "notifications-link", + "/file-share": "file-share-link", }; const activeId = activeMap[path]; if (activeId) { diff --git a/Overlord-Server/public/assets/nav/role-ui.js b/Overlord-Server/public/assets/nav/role-ui.js index 321db7c..4ba7a41 100644 --- a/Overlord-Server/public/assets/nav/role-ui.js +++ b/Overlord-Server/public/assets/nav/role-ui.js @@ -10,6 +10,7 @@ export function applyUserRoleUI(user, refs) { logsLink, notificationsLink, enrollmentLink, + fileShareLink, } = refs; if (!user || !usernameDisplay || !roleBadge) return; @@ -77,5 +78,6 @@ export function applyUserRoleUI(user, refs) { } if (user.role !== "viewer") { scriptsLink?.classList.remove("hidden"); + fileShareLink?.classList.remove("hidden"); } } diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js index 5396c59..9d65212 100644 --- a/Overlord-Server/public/assets/nav/template.js +++ b/Overlord-Server/public/assets/nav/template.js @@ -40,6 +40,9 @@ function mountTopbar(host) { Proxies + @@ -104,6 +107,7 @@ function mountTopbar(host) { notificationsLink: document.getElementById("notifications-link"), enrollmentLink: document.getElementById("enrollment-link"), enrollmentBadge: document.getElementById("enrollment-badge"), + fileShareLink: document.getElementById("file-share-link"), }; } @@ -141,6 +145,10 @@ function mountSidebar(host) { Proxies +