diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b601a8..981a8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## Changes 4/19/2025 + +- **Extended “Remember Me” cookie behavior** + In `AuthController::finalizeLogin()`, after setting `remember_me_token` re‑issued the PHP session cookie with the same 30‑day expiry and called `session_regenerate_id(true)`. + +- **Fetch URL fixes** + Changed all front‑end `fetch("api/…")` calls to absolute paths `fetch("/api/…")` to avoid relative‑path 404/403 issues. + +- **CSRF token refresh** + Updated `submitLogin()` and both TOTP submission handlers to `async/await` a fresh CSRF token from `/api/auth/token.php` (with `credentials: "include"`) immediately before any POST. + +- **submitLogin() overhaul** + Refactored to: + 1. Fetch CSRF + 2. POST credentials to `/api/auth/auth.php` + 3. On `totp_required`, re‑fetch CSRF *again* before calling `openTOTPLoginModal()` + 4. Handle full logins vs. TOTP flows cleanly. + +- **TOTP handlers update** + In both the “Confirm TOTP” button flow and the auto‑submit on 6‑digit input: + - Refreshed CSRF token before every `/api/totp_verify.php` call + - Checked `response.ok` before parsing JSON + - Improved `.catch` error handling + +- **verifyTOTP() endpoint enhancement** + Inside the **pending‑login** branch of `verifyTOTP()`: + - Pulled `$_SESSION['pending_login_remember_me']` + - If true, wrote the persistent token store, set `remember_me_token`, re‑issued the session cookie, and regenerated the session ID + - Cleaned up pending session variables + + --- + ## Changes 4/18/2025 ### fileListView.js diff --git a/public/js/auth.js b/public/js/auth.js index 3ece6b5..ed85832 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -95,7 +95,7 @@ function updateLoginOptionsUIFromStorage() { } export function loadAdminConfigFunc() { - return fetch("api/admin/getConfig.php", { credentials: "include" }) + return fetch("/api/admin/getConfig.php", { credentials: "include" }) .then(response => response.json()) .then(config => { localStorage.setItem("headerTitle", config.header_title || "FileRise"); @@ -105,7 +105,7 @@ export function loadAdminConfigFunc() { localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth); localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin); localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise"); - + updateLoginOptionsUIFromStorage(); const headerTitleElem = document.querySelector(".header-title h1"); @@ -149,9 +149,9 @@ function updateAuthenticatedUI(data) { if (data.username) { localStorage.setItem("username", data.username); } - if (typeof data.folderOnly !== "undefined") { - localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false"); - localStorage.setItem("readOnly", data.readOnly ? "true" : "false"); + if (typeof data.folderOnly !== "undefined") { + localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false"); + localStorage.setItem("readOnly", data.readOnly ? "true" : "false"); localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false"); } @@ -198,11 +198,11 @@ function updateAuthenticatedUI(data) { userPanelBtn.classList.add("btn", "btn-user"); userPanelBtn.setAttribute("data-i18n-title", "user_panel"); userPanelBtn.innerHTML = 'account_circle'; - + const adminBtn = document.getElementById("adminPanelBtn"); if (adminBtn) insertAfter(userPanelBtn, adminBtn); else if (firstButton) insertAfter(userPanelBtn, firstButton); - else headerButtons.appendChild(userPanelBtn); + else headerButtons.appendChild(userPanelBtn); userPanelBtn.addEventListener("click", openUserPanel); } else { userPanelBtn.style.display = "block"; @@ -214,7 +214,7 @@ function updateAuthenticatedUI(data) { } function checkAuthentication(showLoginToast = true) { - return sendRequest("api/auth/checkAuth.php") + return sendRequest("/api/auth/checkAuth.php") .then(data => { if (data.setup) { window.setupMode = true; @@ -228,9 +228,9 @@ function checkAuthentication(showLoginToast = true) { } window.setupMode = false; if (data.authenticated) { - localStorage.setItem("folderOnly", data.folderOnly ); - localStorage.setItem("readOnly", data.readOnly ); - localStorage.setItem("disableUpload",data.disableUpload); + localStorage.setItem("folderOnly", data.folderOnly); + localStorage.setItem("readOnly", data.readOnly); + localStorage.setItem("disableUpload", data.disableUpload); updateLoginOptionsUIFromStorage(); if (typeof data.totp_enabled !== "undefined") { localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); @@ -251,55 +251,71 @@ function checkAuthentication(showLoginToast = true) { } /* ----------------- Authentication Submission ----------------- */ -function submitLogin(data) { +async function submitLogin(data) { setLastLoginData(data); window.__lastLoginData = data; - sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) - .then(response => { - if (response.success || response.status === "ok") { - sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!"); - // Fetch and update permissions, then reload. - sendRequest("api/getUserPermissions.php", "GET") - .then(permissionData => { - if (permissionData && typeof permissionData === "object") { - localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false"); - localStorage.setItem("readOnly", permissionData.readOnly ? "true" : "false"); - localStorage.setItem("disableUpload", permissionData.disableUpload ? "true" : "false"); - } - }) - .catch(() => { - // ignore permission‐fetch errors - }) - .finally(() => { - window.location.reload(); - }); - } else if (response.totp_required) { - openTOTPLoginModal(); - } else if (response.error && response.error.includes("Too many failed login attempts")) { - showToast(response.error); - const loginButton = document.querySelector("#authForm button[type='submit']"); - if (loginButton) { - loginButton.disabled = true; - setTimeout(() => { - loginButton.disabled = false; - showToast("You can now try logging in again."); - }, 30 * 60 * 1000); + try { + // ─── 1) Get CSRF for the initial auth call ─── + let res = await fetch("/api/auth/token.php", { credentials: "include" }); + if (!res.ok) throw new Error("Could not fetch CSRF token"); + window.csrfToken = (await res.json()).csrf_token; + + // ─── 2) Send credentials ─── + const response = await sendRequest( + "/api/auth/auth.php", + "POST", + data, + { "X-CSRF-Token": window.csrfToken } + ); + + // ─── 3a) Full login (no TOTP) ─── + if (response.success || response.status === "ok") { + sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!"); + // … fetch permissions & reload … + try { + const perm = await sendRequest("/api/getUserPermissions.php", "GET"); + if (perm && typeof perm === "object") { + localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false"); + localStorage.setItem("readOnly", perm.readOnly ? "true" : "false"); + localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false"); } - } else { - showToast("Login failed: " + (response.error || "Unknown error")); + } catch {} + return window.location.reload(); + } + + // ─── 3b) TOTP required ─── + if (response.totp_required) { + // **Refresh** CSRF before the TOTP verify call + res = await fetch("/api/auth/token.php", { credentials: "include" }); + if (res.ok) { + window.csrfToken = (await res.json()).csrf_token; } - }) - .catch(err => { - // err may be an Error object or a string - let msg = "Unknown error"; - if (err && typeof err === "object") { - msg = err.error || err.message || msg; - } else if (typeof err === "string") { - msg = err; + // now open the modal—any totp_verify fetch from here on will use the new token + return openTOTPLoginModal(); + } + + // ─── 3c) Too many attempts ─── + if (response.error && response.error.includes("Too many failed login attempts")) { + showToast(response.error); + const btn = document.querySelector("#authForm button[type='submit']"); + if (btn) { + btn.disabled = true; + setTimeout(() => { + btn.disabled = false; + showToast("You can now try logging in again."); + }, 30 * 60 * 1000); } - showToast(`Login failed: ${msg}`); - }); + return; + } + + // ─── 3d) Other failures ─── + showToast("Login failed: " + (response.error || "Unknown error")); + + } catch (err) { + const msg = err.message || err.error || "Unknown error"; + showToast(`Login failed: ${msg}`); + } } window.submitLogin = submitLogin; @@ -327,7 +343,7 @@ function closeRemoveUserModal() { function loadUserList() { // Updated path: from "getUsers.php" to "api/getUsers.php" - fetch("api/getUsers.php", { credentials: "include" }) + fetch("/api/getUsers.php", { credentials: "include" }) .then(response => response.json()) .then(data => { // Assuming the endpoint returns an array of users. @@ -368,7 +384,7 @@ function initAuth() { }); } document.getElementById("logoutBtn").addEventListener("click", function () { - fetch("api/auth/logout.php", { + fetch("/api/auth/logout.php", { method: "POST", credentials: "include", headers: { "X-CSRF-Token": window.csrfToken } @@ -387,7 +403,7 @@ function initAuth() { showToast("Username and password are required!"); return; } - let url = "api/addUser.php"; + let url = "/api/addUser.php"; if (window.setupMode) url += "?setup=1"; fetch(url, { method: "POST", @@ -422,7 +438,7 @@ function initAuth() { } const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?"); if (!confirmed) return; - fetch("api/removeUser.php", { + fetch("/api/removeUser.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, @@ -461,7 +477,7 @@ function initAuth() { return; } const data = { oldPassword, newPassword, confirmPassword }; - fetch("api/changePassword.php", { + fetch("/api/changePassword.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, diff --git a/public/js/authModals.js b/public/js/authModals.js index e529e1a..43d9f2d 100644 --- a/public/js/authModals.js +++ b/public/js/authModals.js @@ -3,8 +3,7 @@ import { sendRequest } from './networkUtils.js'; import { t, applyTranslations, setLocale } from './i18n.js'; import { loadAdminConfigFunc } from './auth.js'; -const version = "v1.2.0"; -// Use t() for the admin panel title. (Make sure t("admin_panel") returns "Admin Panel" in English.) +const version = "v1.2.1"; // Update this version string as needed const adminTitle = `${t("admin_panel")} ${version}`; let lastLoginData = null; @@ -84,7 +83,7 @@ export function openTOTPLoginModal() { showToast(t("please_enter_recovery_code")); return; } - fetch("api/totp_recover.php", { + fetch("/api/totp_recover.php", { method: "POST", credentials: "include", headers: { @@ -110,36 +109,47 @@ export function openTOTPLoginModal() { // TOTP submission const totpInput = document.getElementById("totpLoginInput"); totpInput.focus(); - totpInput.addEventListener("input", function () { + + totpInput.addEventListener("input", async function () { const code = this.value.trim(); - if (code.length === 6) { - fetch("api/totp_verify.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ totp_code: code }) - }) - .then(res => res.json()) - .then(json => { - if (json.status === "ok") { - window.location.href = "/index.html"; - } else { - showToast(json.message || t("totp_verification_failed")); - this.value = ""; - totpLoginModal.style.display = "flex"; - totpInput.focus(); - } - }) - .catch(() => { - showToast(t("totp_verification_failed")); - this.value = ""; - totpLoginModal.style.display = "flex"; - totpInput.focus(); - }); + if (code.length !== 6) { + + return; } + + const tokenRes = await fetch("/api/auth/token.php", { + credentials: "include" + }); + if (!tokenRes.ok) { + showToast(t("totp_verification_failed")); + return; + } + window.csrfToken = (await tokenRes.json()).csrf_token; + + const res = await fetch("/api/totp_verify.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ totp_code: code }) + }); + + if (res.ok) { + const json = await res.json(); + if (json.status === "ok") { + window.location.href = "/index.html"; + return; + } + showToast(json.message || t("totp_verification_failed")); + } else { + showToast(t("totp_verification_failed")); + } + + this.value = ""; + totpLoginModal.style.display = "flex"; + this.focus(); }); } else { // Re-open existing modal @@ -241,7 +251,7 @@ export function openUserPanel() { totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true"; totpCheckbox.addEventListener("change", function () { localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false"); - fetch("api/updateUserPanel.php", { + fetch("/api/updateUserPanel.php", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, @@ -354,13 +364,24 @@ export function openTOTPModal() { closeTOTPModal(true); }); - document.getElementById("confirmTOTPBtn").addEventListener("click", function () { + document.getElementById("confirmTOTPBtn").addEventListener("click", async function () { const code = document.getElementById("totpConfirmInput").value.trim(); if (code.length !== 6) { showToast(t("please_enter_valid_code")); return; } - fetch("api/totp_verify.php", { + + const tokenRes = await fetch("/api/auth/token.php", { + credentials: "include" + }); + if (!tokenRes.ok) { + showToast(t("error_verifying_totp_code")); + return; + } + const { csrf_token } = await tokenRes.json(); + window.csrfToken = csrf_token; + + const verifyRes = await fetch("/api/totp_verify.php", { method: "POST", credentials: "include", headers: { @@ -368,36 +389,40 @@ export function openTOTPModal() { "X-CSRF-Token": window.csrfToken }, body: JSON.stringify({ totp_code: code }) - }) - .then(r => r.json()) - .then(result => { - if (result.status === 'ok') { - showToast(t("totp_enabled_successfully")); - // After successful TOTP verification, fetch the recovery code - fetch("api/totp_saveCode.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - } - }) - .then(r => r.json()) - .then(data => { - if (data.status === 'ok' && data.recoveryCode) { - // Show the recovery code in a secure modal - showRecoveryCodeModal(data.recoveryCode); - } else { - showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error"))); - } - }) - .catch(() => { showToast(t("error_generating_recovery_code")); }); - closeTOTPModal(false); - } else { - showToast(t("totp_verification_failed") + ": " + (result.message || t("invalid_code"))); - } - }) - .catch(() => { showToast(t("error_verifying_totp_code")); }); + }); + + if (!verifyRes.ok) { + showToast(t("totp_verification_failed")); + return; + } + const result = await verifyRes.json(); + if (result.status !== "ok") { + showToast(result.message || t("totp_verification_failed")); + return; + } + + showToast(t("totp_enabled_successfully")); + + const saveRes = await fetch("/api/totp_saveCode.php", { + method: "POST", + credentials: "include", + headers: { + "X-CSRF-Token": window.csrfToken + } + }); + if (!saveRes.ok) { + showToast(t("error_generating_recovery_code")); + closeTOTPModal(false); + return; + } + const data = await saveRes.json(); + if (data.status === "ok" && data.recoveryCode) { + showRecoveryCodeModal(data.recoveryCode); + } else { + showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error"))); + } + + closeTOTPModal(false); }); // Focus the input and attach enter key listener @@ -438,7 +463,7 @@ export function openTOTPModal() { } function loadTOTPQRCode() { - fetch("api/totp_setup.php", { + fetch("/api/totp_setup.php", { method: "GET", credentials: "include", headers: { @@ -477,7 +502,7 @@ export function closeTOTPModal(disable = true) { localStorage.setItem("userTOTPEnabled", "false"); } // Call endpoint to remove the TOTP secret from the user's record - fetch("api/totp_disable.php", { + fetch("/api/totp_disable.php", { method: "POST", credentials: "include", headers: { @@ -563,7 +588,7 @@ function showCustomConfirmModal(message) { } export function openAdminPanel() { - fetch("api/admin/getConfig.php", { credentials: "include" }) + fetch("/api/admin/getConfig.php", { credentials: "include" }) .then(response => response.json()) .then(config => { if (config.header_title) { @@ -725,7 +750,7 @@ export function openAdminPanel() { const disableBasicAuth = disableBasicAuthCheckbox.checked; const disableOIDCLogin = disableOIDCLoginCheckbox.checked; const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim(); - sendRequest("api/admin/updateConfig.php", "POST", { + sendRequest("/api/admin/updateConfig.php", "POST", { header_title: newHeaderTitle, oidc: newOIDCConfig, disableFormLogin, @@ -898,7 +923,7 @@ export function openUserPermissionsModal() { }); }); // Send the permissionsData to the server. - sendRequest("api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken }) + sendRequest("/api/updateUserPermissions.php", "POST", { permissions: permissionsData }, { "X-CSRF-Token": window.csrfToken }) .then(response => { if (response.success) { showToast(t("user_permissions_updated_successfully")); @@ -924,11 +949,11 @@ function loadUserPermissionsList() { listContainer.innerHTML = ""; // First, fetch the current permissions from the server. - fetch("api/getUserPermissions.php", { credentials: "include" }) + fetch("/api/getUserPermissions.php", { credentials: "include" }) .then(response => response.json()) .then(permissionsData => { // Then, fetch the list of users. - return fetch("api/getUsers.php", { credentials: "include" }) + return fetch("/api/getUsers.php", { credentials: "include" }) .then(response => response.json()) .then(usersData => { const users = Array.isArray(usersData) ? usersData : (usersData.users || []); diff --git a/public/js/fileActions.js b/public/js/fileActions.js index 8e994f5..44914a3 100644 --- a/public/js/fileActions.js +++ b/public/js/fileActions.js @@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", function () { const confirmDelete = document.getElementById("confirmDeleteFiles"); if (confirmDelete) { confirmDelete.addEventListener("click", function () { - fetch("api/file/deleteFiles.php", { + fetch("/api/file/deleteFiles.php", { method: "POST", credentials: "include", headers: { @@ -178,7 +178,7 @@ export function handleExtractZipSelected(e) { // Show the progress modal. document.getElementById("downloadProgressModal").style.display = "block"; - fetch("api/file/extractZip.php", { + fetch("/api/file/extractZip.php", { method: "POST", credentials: "include", headers: { @@ -245,7 +245,7 @@ document.addEventListener("DOMContentLoaded", function () { console.log("Download confirmed. Showing progress modal."); document.getElementById("downloadProgressModal").style.display = "block"; const folder = window.currentFolder || "root"; - fetch("api/file/downloadZip.php", { + fetch("/api/file/downloadZip.php", { method: "POST", credentials: "include", headers: { @@ -309,7 +309,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) { if (window.userFolderOnly) { const username = localStorage.getItem("username") || "root"; try { - const response = await fetch("api/folder/getFolderList.php?restricted=1"); + const response = await fetch("/api/folder/getFolderList.php?restricted=1"); let folders = await response.json(); if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { folders = folders.map(item => item.folder); @@ -339,7 +339,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) { } try { - const response = await fetch("api/folder/getFolderList.php"); + const response = await fetch("/api/folder/getFolderList.php"); let folders = await response.json(); if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { folders = folders.map(item => item.folder); @@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () { showToast("Error: Cannot copy files to the same folder."); return; } - fetch("api/file/copyFiles.php", { + fetch("/api/file/copyFiles.php", { method: "POST", credentials: "include", headers: { @@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () { showToast("Error: Cannot move files to the same folder."); return; } - fetch("api/file/moveFiles.php", { + fetch("/api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { @@ -514,7 +514,7 @@ document.addEventListener("DOMContentLoaded", () => { return; } const folderUsed = window.fileFolder; - fetch("api/file/renameFile.php", { + fetch("/api/file/renameFile.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/fileDragDrop.js b/public/js/fileDragDrop.js index 9766716..2aab0cf 100644 --- a/public/js/fileDragDrop.js +++ b/public/js/fileDragDrop.js @@ -96,7 +96,7 @@ export function folderDropHandler(event) { return; } if (!dragData || !dragData.fileName) return; - fetch("api/file/moveFiles.php", { + fetch("/api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/fileEditor.js b/public/js/fileEditor.js index 8c9e88c..ca74af4 100644 --- a/public/js/fileEditor.js +++ b/public/js/fileEditor.js @@ -160,7 +160,7 @@ export function saveFile(fileName, folder) { content: editor.getValue(), folder: folderUsed }; - fetch("api/file/saveFile.php", { + fetch("/api/file/saveFile.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/fileListView.js b/public/js/fileListView.js index 494fef0..2dc535f 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -196,7 +196,7 @@ export function loadFileList(folderParam) { fileListContainer.style.visibility = "hidden"; fileListContainer.innerHTML = "
Loading files...
"; - return fetch("api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) + return fetch("/api/file/getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) .then(response => { if (response.status === 401) { showToast("Session expired. Please log in again."); diff --git a/public/js/filePreview.js b/public/js/filePreview.js index 66a97d2..df18920 100644 --- a/public/js/filePreview.js +++ b/public/js/filePreview.js @@ -48,7 +48,7 @@ export function openShareModal(file, folder) { document.getElementById("generateShareLinkBtn").addEventListener("click", () => { const expiration = document.getElementById("shareExpiration").value; const password = document.getElementById("sharePassword").value; - fetch("api/file/createShareLink.php", { + fetch("/api/file/createShareLink.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/fileTags.js b/public/js/fileTags.js index 6051c6a..7b799b7 100644 --- a/public/js/fileTags.js +++ b/public/js/fileTags.js @@ -272,7 +272,7 @@ function removeGlobalTag(tagName) { // NEW: Save global tag removal to the server. function saveGlobalTagRemoval(tagName) { - fetch("api/file/saveFileTag.php", { + fetch("/api/file/saveFileTag.php", { method: "POST", credentials: "include", headers: { @@ -316,7 +316,7 @@ if (localStorage.getItem('globalTags')) { // New function to load global tags from the server's persistent JSON. export function loadGlobalTags() { - fetch("api/file/getFileTag.php", { credentials: "include" }) + fetch("/api/file/getFileTag.php", { credentials: "include" }) .then(response => { if (!response.ok) { // If the file doesn't exist, assume there are no global tags. @@ -449,7 +449,7 @@ export function saveFileTags(file, deleteGlobal = false, tagToDelete = null) { payload.deleteGlobal = true; payload.tagToDelete = tagToDelete; } - fetch("api/file/saveFileTag.php", { + fetch("/api/file/saveFileTag.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/folderManager.js b/public/js/folderManager.js index 7c27d18..4be16cf 100644 --- a/public/js/folderManager.js +++ b/public/js/folderManager.js @@ -154,7 +154,7 @@ function breadcrumbDropHandler(e) { } const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); if (filesToMove.length === 0) return; - fetch("api/file/moveFiles.php", { + fetch("/api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { @@ -202,7 +202,7 @@ function checkUserFolderPermission() { window.currentFolder = username; return Promise.resolve(true); } - return fetch("api/getUserPermissions.php", { credentials: "include" }) + return fetch("/api/getUserPermissions.php", { credentials: "include" }) .then(response => response.json()) .then(permissionsData => { console.log("checkUserFolderPermission: permissionsData =", permissionsData); @@ -302,7 +302,7 @@ function folderDropHandler(event) { } const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); if (filesToMove.length === 0) return; - fetch("api/file/moveFiles.php", { + fetch("/api/file/moveFiles.php", { method: "POST", credentials: "include", headers: { @@ -353,7 +353,7 @@ export async function loadFolderTree(selectedFolder) { } // Build fetch URL. - let fetchUrl = 'api/folder/getFolderList.php'; + let fetchUrl = '/api/folder/getFolderList.php'; if (window.userFolderOnly) { fetchUrl += '?restricted=1'; } @@ -547,7 +547,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function showToast("CSRF token not loaded yet! Please try again."); return; } - fetch("api/folder/renameFolder.php", { + fetch("/api/folder/renameFolder.php", { method: "POST", credentials: "include", headers: { @@ -592,7 +592,7 @@ attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder"); document.getElementById("confirmDeleteFolder").addEventListener("click", function () { const selectedFolder = window.currentFolder || "root"; const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); - fetch("api/folder/deleteFolder.php", { + fetch("/api/folder/deleteFolder.php", { method: "POST", headers: { "Content-Type": "application/json", @@ -639,7 +639,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", function fullFolderName = selectedFolder + "/" + folderInput; } const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); - fetch("api/folder/createFolder.php", { + fetch("/api/folder/createFolder.php", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/public/js/folderShareModal.js b/public/js/folderShareModal.js index f2d46c9..1cfb8a1 100644 --- a/public/js/folderShareModal.js +++ b/public/js/folderShareModal.js @@ -64,7 +64,7 @@ export function openFolderShareModal(folder) { return; } // Post to the createFolderShareLink endpoint. - fetch("api/folder/createShareFolderLink.php", { + fetch("/api/folder/createShareFolderLink.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/main.js b/public/js/main.js index 5875a9d..742ff74 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -14,7 +14,7 @@ import { t, applyTranslations, setLocale } from './i18n.js'; // Remove the retry logic version and just use loadCsrfToken directly: function loadCsrfToken() { - return fetch('api/auth/token.php', { credentials: 'include' }) + return fetch('/api/auth/token.php', { credentials: 'include' }) .then(response => { if (!response.ok) { throw new Error("Token fetch failed with status: " + response.status); diff --git a/public/js/trashRestoreDelete.js b/public/js/trashRestoreDelete.js index d889a49..f05da5a 100644 --- a/public/js/trashRestoreDelete.js +++ b/public/js/trashRestoreDelete.js @@ -69,7 +69,7 @@ export function setupTrashRestoreDelete() { showToast(t("no_trash_selected")); return; } - fetch("api/file/restoreFiles.php", { + fetch("/api/file/restoreFiles.php", { method: "POST", credentials: "include", headers: { @@ -109,7 +109,7 @@ export function setupTrashRestoreDelete() { showToast(t("trash_empty")); return; } - fetch("api/file/restoreFiles.php", { + fetch("/api/file/restoreFiles.php", { method: "POST", credentials: "include", headers: { @@ -151,7 +151,7 @@ export function setupTrashRestoreDelete() { return; } showConfirm("Are you sure you want to permanently delete the selected trash items?", () => { - fetch("api/file/deleteTrashFiles.php", { + fetch("/api/file/deleteTrashFiles.php", { method: "POST", credentials: "include", headers: { @@ -186,7 +186,7 @@ export function setupTrashRestoreDelete() { if (deleteAllBtn) { deleteAllBtn.addEventListener("click", () => { showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => { - fetch("api/file/deleteTrashFiles.php", { + fetch("/api/file/deleteTrashFiles.php", { method: "POST", credentials: "include", headers: { @@ -234,7 +234,7 @@ export function setupTrashRestoreDelete() { * Loads trash items from the server and updates the restore modal list. */ export function loadTrashItems() { - fetch("api/file/getTrashItems.php", { credentials: "include" }) + fetch("/api/file/getTrashItems.php", { credentials: "include" }) .then(response => response.json()) .then(trashItems => { const listContainer = document.getElementById("restoreFilesList"); @@ -271,7 +271,7 @@ export function loadTrashItems() { * Automatically purges (permanently deletes) trash items older than 3 days. */ function autoPurgeOldTrash() { - fetch("api/file/getTrashItems.php", { credentials: "include" }) + fetch("/api/file/getTrashItems.php", { credentials: "include" }) .then(response => response.json()) .then(trashItems => { const now = Date.now(); @@ -279,7 +279,7 @@ function autoPurgeOldTrash() { const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays); if (oldItems.length > 0) { const files = oldItems.map(item => item.trashName); - fetch("api/file/deleteTrashFiles.php", { + fetch("/api/file/deleteTrashFiles.php", { method: "POST", credentials: "include", headers: { diff --git a/public/js/upload.js b/public/js/upload.js index ba43a76..671afef 100644 --- a/public/js/upload.js +++ b/public/js/upload.js @@ -126,7 +126,7 @@ function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, int // Prefix with "resumable_" to match your PHP regex. params.append('folder', 'resumable_' + identifier); params.append('csrf_token', csrfToken); - fetch('api/upload/removeChunks.php', { + fetch('/api/upload/removeChunks.php', { method: 'POST', credentials: 'include', headers: { @@ -664,7 +664,7 @@ function submitFiles(allFiles) { } }); - xhr.open("POST", "api/upload/upload.php", true); + xhr.open("POST", "/api/upload/upload.php", true); xhr.setRequestHeader("X-CSRF-Token", window.csrfToken); xhr.send(formData); }); diff --git a/src/controllers/authController.php b/src/controllers/authController.php index f9bd58a..5645dfa 100644 --- a/src/controllers/authController.php +++ b/src/controllers/authController.php @@ -238,22 +238,39 @@ class AuthController $token = bin2hex(random_bytes(32)); $expiry = time() + 30 * 24 * 60 * 60; $all = []; + if (file_exists($tokFile)) { $dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']); $all = json_decode($dec, true) ?: []; } + $all[$token] = [ 'username' => $username, - 'expiry' => $expiry, - 'isAdmin' => $_SESSION['isAdmin'] + 'expiry' => $expiry, + 'isAdmin' => $_SESSION['isAdmin'] ]; + file_put_contents( $tokFile, encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']), LOCK_EX ); - $secure = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + + $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true); + + setcookie( + session_name(), + session_id(), + $expiry, + '/', + '', + $secure, + true + ); + + session_regenerate_id(true); } echo json_encode([ diff --git a/src/controllers/userController.php b/src/controllers/userController.php index 6377f90..4ec1afb 100644 --- a/src/controllers/userController.php +++ b/src/controllers/userController.php @@ -847,104 +847,147 @@ class UserController * ) */ - public function verifyTOTP() - { - header('Content-Type: application/json'); - // Set CSP headers if desired: - header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';"); - - // Rate‑limit: initialize totp_failures if not set. - if (!isset($_SESSION['totp_failures'])) { - $_SESSION['totp_failures'] = 0; - } - if ($_SESSION['totp_failures'] >= 5) { - http_response_code(429); - echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']); - exit; - } - - // Must be authenticated OR have a pending login. - if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) { - http_response_code(403); - echo json_encode(['status' => 'error', 'message' => 'Not authenticated']); - exit; - } - - // CSRF check. - $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); - $csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; - if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { - http_response_code(403); - echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']); - exit; - } - - // Parse input. - $inputData = json_decode(file_get_contents("php://input"), true); - $code = trim($inputData['totp_code'] ?? ''); - if (!preg_match('/^\d{6}$/', $code)) { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']); - exit; - } - - // Create TFA object. - $tfa = new \RobThree\Auth\TwoFactorAuth( - new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), - 'FileRise', - 6, - 30, - \RobThree\Auth\Algorithm::Sha1 - ); - - // Check if we are in pending login flow. - if (isset($_SESSION['pending_login_user'])) { - $username = $_SESSION['pending_login_user']; - $pendingSecret = $_SESSION['pending_login_secret'] ?? null; - if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) { - $_SESSION['totp_failures']++; - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); - exit; - } - // Successful pending login: finalize login. - session_regenerate_id(true); - $_SESSION['authenticated'] = true; - $_SESSION['username'] = $username; - // Set isAdmin based on user role. - $_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1"); - // Load additional permissions (e.g., folderOnly) as needed. - $_SESSION['folderOnly'] = loadUserPermissions($username); - unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']); - echo json_encode(['status' => 'ok', 'message' => 'Login successful']); - exit; - } - - // Otherwise, we are in setup/verification flow. - $username = $_SESSION['username'] ?? ''; - if (!$username) { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Username not found in session']); - exit; - } - - // Retrieve the user's TOTP secret from the model. - $totpSecret = userModel::getTOTPSecret($username); - if (!$totpSecret) { - http_response_code(500); - echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']); - exit; - } - - if (!$tfa->verifyCode($totpSecret, $code)) { - $_SESSION['totp_failures']++; - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); - exit; - } - - // Successful verification. - unset($_SESSION['totp_failures']); - echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']); - } + public function verifyTOTP() + { + header('Content-Type: application/json'); + header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';"); + + // Rate‑limit + if (!isset($_SESSION['totp_failures'])) { + $_SESSION['totp_failures'] = 0; + } + if ($_SESSION['totp_failures'] >= 5) { + http_response_code(429); + echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']); + exit; + } + + // Must be authenticated OR pending login + if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Not authenticated']); + exit; + } + + // CSRF check + $headersArr = array_change_key_case(getallheaders(), CASE_LOWER); + $csrfHeader = $headersArr['x-csrf-token'] ?? ''; + if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { + http_response_code(403); + echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']); + exit; + } + + // Parse and validate input + $inputData = json_decode(file_get_contents("php://input"), true); + $code = trim($inputData['totp_code'] ?? ''); + if (!preg_match('/^\d{6}$/', $code)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']); + exit; + } + + // TFA helper + $tfa = new \RobThree\Auth\TwoFactorAuth( + new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), + 'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1 + ); + + // Pending‑login flow (first password step passed) + if (isset($_SESSION['pending_login_user'])) { + $username = $_SESSION['pending_login_user']; + $pendingSecret = $_SESSION['pending_login_secret'] ?? null; + $rememberMe = $_SESSION['pending_login_remember_me'] ?? false; + + if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) { + $_SESSION['totp_failures']++; + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); + exit; + } + + // === Issue “remember me” token if requested === + if ($rememberMe) { + $tokFile = USERS_DIR . 'persistent_tokens.json'; + $token = bin2hex(random_bytes(32)); + $expiry = time() + 30 * 24 * 60 * 60; + $all = []; + + if (file_exists($tokFile)) { + $dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']); + $all = json_decode($dec, true) ?: []; + } + $all[$token] = [ + 'username' => $username, + 'expiry' => $expiry, + 'isAdmin' => $_SESSION['isAdmin'] + ]; + file_put_contents( + $tokFile, + encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']), + LOCK_EX + ); + + $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + + // Persistent cookie + setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true); + + // Re‑issue PHP session cookie + setcookie( + session_name(), + session_id(), + $expiry, + '/', + '', + $secure, + true + ); + } + + // Finalize login + session_regenerate_id(true); + $_SESSION['authenticated'] = true; + $_SESSION['username'] = $username; + $_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1"); + $_SESSION['folderOnly'] = loadUserPermissions($username); + + // Clean up + unset( + $_SESSION['pending_login_user'], + $_SESSION['pending_login_secret'], + $_SESSION['pending_login_remember_me'], + $_SESSION['totp_failures'] + ); + + echo json_encode(['status' => 'ok', 'message' => 'Login successful']); + exit; + } + + // Setup/verification flow (not pending) + $username = $_SESSION['username'] ?? ''; + if (!$username) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Username not found in session']); + exit; + } + + $totpSecret = userModel::getTOTPSecret($username); + if (!$totpSecret) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']); + exit; + } + + if (!$tfa->verifyCode($totpSecret, $code)) { + $_SESSION['totp_failures']++; + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); + exit; + } + + // Successful setup/verification + unset($_SESSION['totp_failures']); + echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']); + } }