Fetch URL fixes, Extended “Remember Me” cookie behavior, submitLogin() overhaul

This commit is contained in:
Ryan
2025-04-19 17:53:01 -04:00
committed by GitHub
parent e390a35e8a
commit 61357af203
16 changed files with 399 additions and 266 deletions

View File

@@ -1,5 +1,37 @@
# Changelog # Changelog
## Changes 4/19/2025
- **Extended “Remember Me” cookie behavior**
In `AuthController::finalizeLogin()`, after setting `remember_me_token` reissued the PHP session cookie with the same 30day expiry and called `session_regenerate_id(true)`.
- **Fetch URL fixes**
Changed all frontend `fetch("api/…")` calls to absolute paths `fetch("/api/…")` to avoid relativepath 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`, refetch 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 autosubmit on 6digit 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 **pendinglogin** branch of `verifyTOTP()`:
- Pulled `$_SESSION['pending_login_remember_me']`
- If true, wrote the persistent token store, set `remember_me_token`, reissued the session cookie, and regenerated the session ID
- Cleaned up pending session variables
---
## Changes 4/18/2025 ## Changes 4/18/2025
### fileListView.js ### fileListView.js

View File

@@ -95,7 +95,7 @@ function updateLoginOptionsUIFromStorage() {
} }
export function loadAdminConfigFunc() { export function loadAdminConfigFunc() {
return fetch("api/admin/getConfig.php", { credentials: "include" }) return fetch("/api/admin/getConfig.php", { credentials: "include" })
.then(response => response.json()) .then(response => response.json())
.then(config => { .then(config => {
localStorage.setItem("headerTitle", config.header_title || "FileRise"); localStorage.setItem("headerTitle", config.header_title || "FileRise");
@@ -105,7 +105,7 @@ export function loadAdminConfigFunc() {
localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth); localStorage.setItem("disableBasicAuth", config.loginOptions.disableBasicAuth);
localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin); localStorage.setItem("disableOIDCLogin", config.loginOptions.disableOIDCLogin);
localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise"); localStorage.setItem("globalOtpauthUrl", config.globalOtpauthUrl || "otpauth://totp/{label}?secret={secret}&issuer=FileRise");
updateLoginOptionsUIFromStorage(); updateLoginOptionsUIFromStorage();
const headerTitleElem = document.querySelector(".header-title h1"); const headerTitleElem = document.querySelector(".header-title h1");
@@ -149,9 +149,9 @@ function updateAuthenticatedUI(data) {
if (data.username) { if (data.username) {
localStorage.setItem("username", data.username); localStorage.setItem("username", data.username);
} }
if (typeof data.folderOnly !== "undefined") { if (typeof data.folderOnly !== "undefined") {
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false"); localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
localStorage.setItem("readOnly", data.readOnly ? "true" : "false"); localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false"); localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
} }
@@ -198,11 +198,11 @@ function updateAuthenticatedUI(data) {
userPanelBtn.classList.add("btn", "btn-user"); userPanelBtn.classList.add("btn", "btn-user");
userPanelBtn.setAttribute("data-i18n-title", "user_panel"); userPanelBtn.setAttribute("data-i18n-title", "user_panel");
userPanelBtn.innerHTML = '<i class="material-icons">account_circle</i>'; userPanelBtn.innerHTML = '<i class="material-icons">account_circle</i>';
const adminBtn = document.getElementById("adminPanelBtn"); const adminBtn = document.getElementById("adminPanelBtn");
if (adminBtn) insertAfter(userPanelBtn, adminBtn); if (adminBtn) insertAfter(userPanelBtn, adminBtn);
else if (firstButton) insertAfter(userPanelBtn, firstButton); else if (firstButton) insertAfter(userPanelBtn, firstButton);
else headerButtons.appendChild(userPanelBtn); else headerButtons.appendChild(userPanelBtn);
userPanelBtn.addEventListener("click", openUserPanel); userPanelBtn.addEventListener("click", openUserPanel);
} else { } else {
userPanelBtn.style.display = "block"; userPanelBtn.style.display = "block";
@@ -214,7 +214,7 @@ function updateAuthenticatedUI(data) {
} }
function checkAuthentication(showLoginToast = true) { function checkAuthentication(showLoginToast = true) {
return sendRequest("api/auth/checkAuth.php") return sendRequest("/api/auth/checkAuth.php")
.then(data => { .then(data => {
if (data.setup) { if (data.setup) {
window.setupMode = true; window.setupMode = true;
@@ -228,9 +228,9 @@ function checkAuthentication(showLoginToast = true) {
} }
window.setupMode = false; window.setupMode = false;
if (data.authenticated) { if (data.authenticated) {
localStorage.setItem("folderOnly", data.folderOnly ); localStorage.setItem("folderOnly", data.folderOnly);
localStorage.setItem("readOnly", data.readOnly ); localStorage.setItem("readOnly", data.readOnly);
localStorage.setItem("disableUpload",data.disableUpload); localStorage.setItem("disableUpload", data.disableUpload);
updateLoginOptionsUIFromStorage(); updateLoginOptionsUIFromStorage();
if (typeof data.totp_enabled !== "undefined") { if (typeof data.totp_enabled !== "undefined") {
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
@@ -251,55 +251,71 @@ function checkAuthentication(showLoginToast = true) {
} }
/* ----------------- Authentication Submission ----------------- */ /* ----------------- Authentication Submission ----------------- */
function submitLogin(data) { async function submitLogin(data) {
setLastLoginData(data); setLastLoginData(data);
window.__lastLoginData = data; window.__lastLoginData = data;
sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken }) try {
.then(response => { // ─── 1) Get CSRF for the initial auth call ───
if (response.success || response.status === "ok") { let res = await fetch("/api/auth/token.php", { credentials: "include" });
sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!"); if (!res.ok) throw new Error("Could not fetch CSRF token");
// Fetch and update permissions, then reload. window.csrfToken = (await res.json()).csrf_token;
sendRequest("api/getUserPermissions.php", "GET")
.then(permissionData => { // ─── 2) Send credentials ───
if (permissionData && typeof permissionData === "object") { const response = await sendRequest(
localStorage.setItem("folderOnly", permissionData.folderOnly ? "true" : "false"); "/api/auth/auth.php",
localStorage.setItem("readOnly", permissionData.readOnly ? "true" : "false"); "POST",
localStorage.setItem("disableUpload", permissionData.disableUpload ? "true" : "false"); data,
} { "X-CSRF-Token": window.csrfToken }
}) );
.catch(() => {
// ignore permissionfetch errors // ─── 3a) Full login (no TOTP) ───
}) if (response.success || response.status === "ok") {
.finally(() => { sessionStorage.setItem("welcomeMessage", "Welcome back, " + data.username + "!");
window.location.reload(); // … fetch permissions & reload …
}); try {
} else if (response.totp_required) { const perm = await sendRequest("/api/getUserPermissions.php", "GET");
openTOTPLoginModal(); if (perm && typeof perm === "object") {
} else if (response.error && response.error.includes("Too many failed login attempts")) { localStorage.setItem("folderOnly", perm.folderOnly ? "true" : "false");
showToast(response.error); localStorage.setItem("readOnly", perm.readOnly ? "true" : "false");
const loginButton = document.querySelector("#authForm button[type='submit']"); localStorage.setItem("disableUpload",perm.disableUpload? "true" : "false");
if (loginButton) {
loginButton.disabled = true;
setTimeout(() => {
loginButton.disabled = false;
showToast("You can now try logging in again.");
}, 30 * 60 * 1000);
} }
} else { } catch {}
showToast("Login failed: " + (response.error || "Unknown error")); 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;
} }
}) // now open the modal—any totp_verify fetch from here on will use the new token
.catch(err => { return openTOTPLoginModal();
// err may be an Error object or a string }
let msg = "Unknown error";
if (err && typeof err === "object") { // ─── 3c) Too many attempts ───
msg = err.error || err.message || msg; if (response.error && response.error.includes("Too many failed login attempts")) {
} else if (typeof err === "string") { showToast(response.error);
msg = err; 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; window.submitLogin = submitLogin;
@@ -327,7 +343,7 @@ function closeRemoveUserModal() {
function loadUserList() { function loadUserList() {
// Updated path: from "getUsers.php" to "api/getUsers.php" // 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(response => response.json())
.then(data => { .then(data => {
// Assuming the endpoint returns an array of users. // Assuming the endpoint returns an array of users.
@@ -368,7 +384,7 @@ function initAuth() {
}); });
} }
document.getElementById("logoutBtn").addEventListener("click", function () { document.getElementById("logoutBtn").addEventListener("click", function () {
fetch("api/auth/logout.php", { fetch("/api/auth/logout.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { "X-CSRF-Token": window.csrfToken } headers: { "X-CSRF-Token": window.csrfToken }
@@ -387,7 +403,7 @@ function initAuth() {
showToast("Username and password are required!"); showToast("Username and password are required!");
return; return;
} }
let url = "api/addUser.php"; let url = "/api/addUser.php";
if (window.setupMode) url += "?setup=1"; if (window.setupMode) url += "?setup=1";
fetch(url, { fetch(url, {
method: "POST", method: "POST",
@@ -422,7 +438,7 @@ function initAuth() {
} }
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?"); const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
if (!confirmed) return; if (!confirmed) return;
fetch("api/removeUser.php", { fetch("/api/removeUser.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
@@ -461,7 +477,7 @@ function initAuth() {
return; return;
} }
const data = { oldPassword, newPassword, confirmPassword }; const data = { oldPassword, newPassword, confirmPassword };
fetch("api/changePassword.php", { fetch("/api/changePassword.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },

View File

@@ -3,8 +3,7 @@ import { sendRequest } from './networkUtils.js';
import { t, applyTranslations, setLocale } from './i18n.js'; import { t, applyTranslations, setLocale } from './i18n.js';
import { loadAdminConfigFunc } from './auth.js'; import { loadAdminConfigFunc } from './auth.js';
const version = "v1.2.0"; const version = "v1.2.1"; // Update this version string as needed
// Use t() for the admin panel title. (Make sure t("admin_panel") returns "Admin Panel" in English.)
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>`;
let lastLoginData = null; let lastLoginData = null;
@@ -84,7 +83,7 @@ export function openTOTPLoginModal() {
showToast(t("please_enter_recovery_code")); showToast(t("please_enter_recovery_code"));
return; return;
} }
fetch("api/totp_recover.php", { fetch("/api/totp_recover.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -110,36 +109,47 @@ export function openTOTPLoginModal() {
// TOTP submission // TOTP submission
const totpInput = document.getElementById("totpLoginInput"); const totpInput = document.getElementById("totpLoginInput");
totpInput.focus(); totpInput.focus();
totpInput.addEventListener("input", function () {
totpInput.addEventListener("input", async function () {
const code = this.value.trim(); const code = this.value.trim();
if (code.length === 6) { if (code.length !== 6) {
fetch("api/totp_verify.php", {
method: "POST", return;
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();
});
} }
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 { } else {
// Re-open existing modal // Re-open existing modal
@@ -241,7 +251,7 @@ export function openUserPanel() {
totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true"; totpCheckbox.checked = localStorage.getItem("userTOTPEnabled") === "true";
totpCheckbox.addEventListener("change", function () { totpCheckbox.addEventListener("change", function () {
localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false"); localStorage.setItem("userTOTPEnabled", this.checked ? "true" : "false");
fetch("api/updateUserPanel.php", { fetch("/api/updateUserPanel.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, headers: { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken },
@@ -354,13 +364,24 @@ export function openTOTPModal() {
closeTOTPModal(true); closeTOTPModal(true);
}); });
document.getElementById("confirmTOTPBtn").addEventListener("click", function () { document.getElementById("confirmTOTPBtn").addEventListener("click", async function () {
const code = document.getElementById("totpConfirmInput").value.trim(); const code = document.getElementById("totpConfirmInput").value.trim();
if (code.length !== 6) { if (code.length !== 6) {
showToast(t("please_enter_valid_code")); showToast(t("please_enter_valid_code"));
return; 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", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -368,36 +389,40 @@ export function openTOTPModal() {
"X-CSRF-Token": window.csrfToken "X-CSRF-Token": window.csrfToken
}, },
body: JSON.stringify({ totp_code: code }) body: JSON.stringify({ totp_code: code })
}) });
.then(r => r.json())
.then(result => { if (!verifyRes.ok) {
if (result.status === 'ok') { showToast(t("totp_verification_failed"));
showToast(t("totp_enabled_successfully")); return;
// After successful TOTP verification, fetch the recovery code }
fetch("api/totp_saveCode.php", { const result = await verifyRes.json();
method: "POST", if (result.status !== "ok") {
credentials: "include", showToast(result.message || t("totp_verification_failed"));
headers: { return;
"Content-Type": "application/json", }
"X-CSRF-Token": window.csrfToken
} showToast(t("totp_enabled_successfully"));
})
.then(r => r.json()) const saveRes = await fetch("/api/totp_saveCode.php", {
.then(data => { method: "POST",
if (data.status === 'ok' && data.recoveryCode) { credentials: "include",
// Show the recovery code in a secure modal headers: {
showRecoveryCodeModal(data.recoveryCode); "X-CSRF-Token": window.csrfToken
} else { }
showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error"))); });
} if (!saveRes.ok) {
}) showToast(t("error_generating_recovery_code"));
.catch(() => { showToast(t("error_generating_recovery_code")); }); closeTOTPModal(false);
closeTOTPModal(false); return;
} else { }
showToast(t("totp_verification_failed") + ": " + (result.message || t("invalid_code"))); const data = await saveRes.json();
} if (data.status === "ok" && data.recoveryCode) {
}) showRecoveryCodeModal(data.recoveryCode);
.catch(() => { showToast(t("error_verifying_totp_code")); }); } else {
showToast(t("error_generating_recovery_code") + ": " + (data.message || t("unknown_error")));
}
closeTOTPModal(false);
}); });
// Focus the input and attach enter key listener // Focus the input and attach enter key listener
@@ -438,7 +463,7 @@ export function openTOTPModal() {
} }
function loadTOTPQRCode() { function loadTOTPQRCode() {
fetch("api/totp_setup.php", { fetch("/api/totp_setup.php", {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -477,7 +502,7 @@ export function closeTOTPModal(disable = true) {
localStorage.setItem("userTOTPEnabled", "false"); localStorage.setItem("userTOTPEnabled", "false");
} }
// Call endpoint to remove the TOTP secret from the user's record // Call endpoint to remove the TOTP secret from the user's record
fetch("api/totp_disable.php", { fetch("/api/totp_disable.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -563,7 +588,7 @@ function showCustomConfirmModal(message) {
} }
export function openAdminPanel() { export function openAdminPanel() {
fetch("api/admin/getConfig.php", { credentials: "include" }) fetch("/api/admin/getConfig.php", { credentials: "include" })
.then(response => response.json()) .then(response => response.json())
.then(config => { .then(config => {
if (config.header_title) { if (config.header_title) {
@@ -725,7 +750,7 @@ export function openAdminPanel() {
const disableBasicAuth = disableBasicAuthCheckbox.checked; const disableBasicAuth = disableBasicAuthCheckbox.checked;
const disableOIDCLogin = disableOIDCLoginCheckbox.checked; const disableOIDCLogin = disableOIDCLoginCheckbox.checked;
const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim(); const globalOtpauthUrl = document.getElementById("globalOtpauthUrl").value.trim();
sendRequest("api/admin/updateConfig.php", "POST", { sendRequest("/api/admin/updateConfig.php", "POST", {
header_title: newHeaderTitle, header_title: newHeaderTitle,
oidc: newOIDCConfig, oidc: newOIDCConfig,
disableFormLogin, disableFormLogin,
@@ -898,7 +923,7 @@ export function openUserPermissionsModal() {
}); });
}); });
// Send the permissionsData to the server. // 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 => { .then(response => {
if (response.success) { if (response.success) {
showToast(t("user_permissions_updated_successfully")); showToast(t("user_permissions_updated_successfully"));
@@ -924,11 +949,11 @@ function loadUserPermissionsList() {
listContainer.innerHTML = ""; listContainer.innerHTML = "";
// First, fetch the current permissions from the server. // 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(response => response.json())
.then(permissionsData => { .then(permissionsData => {
// Then, fetch the list of users. // 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(response => response.json())
.then(usersData => { .then(usersData => {
const users = Array.isArray(usersData) ? usersData : (usersData.users || []); const users = Array.isArray(usersData) ? usersData : (usersData.users || []);

View File

@@ -32,7 +32,7 @@ document.addEventListener("DOMContentLoaded", function () {
const confirmDelete = document.getElementById("confirmDeleteFiles"); const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) { if (confirmDelete) {
confirmDelete.addEventListener("click", function () { confirmDelete.addEventListener("click", function () {
fetch("api/file/deleteFiles.php", { fetch("/api/file/deleteFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -178,7 +178,7 @@ export function handleExtractZipSelected(e) {
// Show the progress modal. // Show the progress modal.
document.getElementById("downloadProgressModal").style.display = "block"; document.getElementById("downloadProgressModal").style.display = "block";
fetch("api/file/extractZip.php", { fetch("/api/file/extractZip.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -245,7 +245,7 @@ document.addEventListener("DOMContentLoaded", function () {
console.log("Download confirmed. Showing progress modal."); console.log("Download confirmed. Showing progress modal.");
document.getElementById("downloadProgressModal").style.display = "block"; document.getElementById("downloadProgressModal").style.display = "block";
const folder = window.currentFolder || "root"; const folder = window.currentFolder || "root";
fetch("api/file/downloadZip.php", { fetch("/api/file/downloadZip.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -309,7 +309,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) {
if (window.userFolderOnly) { if (window.userFolderOnly) {
const username = localStorage.getItem("username") || "root"; const username = localStorage.getItem("username") || "root";
try { 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(); let folders = await response.json();
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
folders = folders.map(item => item.folder); folders = folders.map(item => item.folder);
@@ -339,7 +339,7 @@ export async function loadCopyMoveFolderListForModal(dropdownId) {
} }
try { try {
const response = await fetch("api/folder/getFolderList.php"); const response = await fetch("/api/folder/getFolderList.php");
let folders = await response.json(); let folders = await response.json();
if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) {
folders = folders.map(item => item.folder); folders = folders.map(item => item.folder);
@@ -397,7 +397,7 @@ document.addEventListener("DOMContentLoaded", function () {
showToast("Error: Cannot copy files to the same folder."); showToast("Error: Cannot copy files to the same folder.");
return; return;
} }
fetch("api/file/copyFiles.php", { fetch("/api/file/copyFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () {
showToast("Error: Cannot move files to the same folder."); showToast("Error: Cannot move files to the same folder.");
return; return;
} }
fetch("api/file/moveFiles.php", { fetch("/api/file/moveFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -514,7 +514,7 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
} }
const folderUsed = window.fileFolder; const folderUsed = window.fileFolder;
fetch("api/file/renameFile.php", { fetch("/api/file/renameFile.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -96,7 +96,7 @@ export function folderDropHandler(event) {
return; return;
} }
if (!dragData || !dragData.fileName) return; if (!dragData || !dragData.fileName) return;
fetch("api/file/moveFiles.php", { fetch("/api/file/moveFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -160,7 +160,7 @@ export function saveFile(fileName, folder) {
content: editor.getValue(), content: editor.getValue(),
folder: folderUsed folder: folderUsed
}; };
fetch("api/file/saveFile.php", { fetch("/api/file/saveFile.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -196,7 +196,7 @@ export function loadFileList(folderParam) {
fileListContainer.style.visibility = "hidden"; fileListContainer.style.visibility = "hidden";
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>"; fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
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 => { .then(response => {
if (response.status === 401) { if (response.status === 401) {
showToast("Session expired. Please log in again."); showToast("Session expired. Please log in again.");

View File

@@ -48,7 +48,7 @@ export function openShareModal(file, folder) {
document.getElementById("generateShareLinkBtn").addEventListener("click", () => { document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
const expiration = document.getElementById("shareExpiration").value; const expiration = document.getElementById("shareExpiration").value;
const password = document.getElementById("sharePassword").value; const password = document.getElementById("sharePassword").value;
fetch("api/file/createShareLink.php", { fetch("/api/file/createShareLink.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -272,7 +272,7 @@ function removeGlobalTag(tagName) {
// NEW: Save global tag removal to the server. // NEW: Save global tag removal to the server.
function saveGlobalTagRemoval(tagName) { function saveGlobalTagRemoval(tagName) {
fetch("api/file/saveFileTag.php", { fetch("/api/file/saveFileTag.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -316,7 +316,7 @@ if (localStorage.getItem('globalTags')) {
// New function to load global tags from the server's persistent JSON. // New function to load global tags from the server's persistent JSON.
export function loadGlobalTags() { export function loadGlobalTags() {
fetch("api/file/getFileTag.php", { credentials: "include" }) fetch("/api/file/getFileTag.php", { credentials: "include" })
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
// If the file doesn't exist, assume there are no global tags. // 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.deleteGlobal = true;
payload.tagToDelete = tagToDelete; payload.tagToDelete = tagToDelete;
} }
fetch("api/file/saveFileTag.php", { fetch("/api/file/saveFileTag.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -154,7 +154,7 @@ function breadcrumbDropHandler(e) {
} }
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return; if (filesToMove.length === 0) return;
fetch("api/file/moveFiles.php", { fetch("/api/file/moveFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -202,7 +202,7 @@ function checkUserFolderPermission() {
window.currentFolder = username; window.currentFolder = username;
return Promise.resolve(true); return Promise.resolve(true);
} }
return fetch("api/getUserPermissions.php", { credentials: "include" }) return fetch("/api/getUserPermissions.php", { credentials: "include" })
.then(response => response.json()) .then(response => response.json())
.then(permissionsData => { .then(permissionsData => {
console.log("checkUserFolderPermission: permissionsData =", permissionsData); console.log("checkUserFolderPermission: permissionsData =", permissionsData);
@@ -302,7 +302,7 @@ function folderDropHandler(event) {
} }
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return; if (filesToMove.length === 0) return;
fetch("api/file/moveFiles.php", { fetch("/api/file/moveFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -353,7 +353,7 @@ export async function loadFolderTree(selectedFolder) {
} }
// Build fetch URL. // Build fetch URL.
let fetchUrl = 'api/folder/getFolderList.php'; let fetchUrl = '/api/folder/getFolderList.php';
if (window.userFolderOnly) { if (window.userFolderOnly) {
fetchUrl += '?restricted=1'; fetchUrl += '?restricted=1';
} }
@@ -547,7 +547,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
showToast("CSRF token not loaded yet! Please try again."); showToast("CSRF token not loaded yet! Please try again.");
return; return;
} }
fetch("api/folder/renameFolder.php", { fetch("/api/folder/renameFolder.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -592,7 +592,7 @@ attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
document.getElementById("confirmDeleteFolder").addEventListener("click", function () { document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root"; const selectedFolder = window.currentFolder || "root";
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch("api/folder/deleteFolder.php", { fetch("/api/folder/deleteFolder.php", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -639,7 +639,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
fullFolderName = selectedFolder + "/" + folderInput; fullFolderName = selectedFolder + "/" + folderInput;
} }
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch("api/folder/createFolder.php", { fetch("/api/folder/createFolder.php", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -64,7 +64,7 @@ export function openFolderShareModal(folder) {
return; return;
} }
// Post to the createFolderShareLink endpoint. // Post to the createFolderShareLink endpoint.
fetch("api/folder/createShareFolderLink.php", { fetch("/api/folder/createShareFolderLink.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -14,7 +14,7 @@ import { t, applyTranslations, setLocale } from './i18n.js';
// Remove the retry logic version and just use loadCsrfToken directly: // Remove the retry logic version and just use loadCsrfToken directly:
function loadCsrfToken() { function loadCsrfToken() {
return fetch('api/auth/token.php', { credentials: 'include' }) return fetch('/api/auth/token.php', { credentials: 'include' })
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error("Token fetch failed with status: " + response.status); throw new Error("Token fetch failed with status: " + response.status);

View File

@@ -69,7 +69,7 @@ export function setupTrashRestoreDelete() {
showToast(t("no_trash_selected")); showToast(t("no_trash_selected"));
return; return;
} }
fetch("api/file/restoreFiles.php", { fetch("/api/file/restoreFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -109,7 +109,7 @@ export function setupTrashRestoreDelete() {
showToast(t("trash_empty")); showToast(t("trash_empty"));
return; return;
} }
fetch("api/file/restoreFiles.php", { fetch("/api/file/restoreFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -151,7 +151,7 @@ export function setupTrashRestoreDelete() {
return; return;
} }
showConfirm("Are you sure you want to permanently delete the selected trash items?", () => { 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", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -186,7 +186,7 @@ export function setupTrashRestoreDelete() {
if (deleteAllBtn) { if (deleteAllBtn) {
deleteAllBtn.addEventListener("click", () => { deleteAllBtn.addEventListener("click", () => {
showConfirm("Are you sure you want to permanently delete all trash items? This action cannot be undone.", () => { 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", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -234,7 +234,7 @@ export function setupTrashRestoreDelete() {
* Loads trash items from the server and updates the restore modal list. * Loads trash items from the server and updates the restore modal list.
*/ */
export function loadTrashItems() { export function loadTrashItems() {
fetch("api/file/getTrashItems.php", { credentials: "include" }) fetch("/api/file/getTrashItems.php", { credentials: "include" })
.then(response => response.json()) .then(response => response.json())
.then(trashItems => { .then(trashItems => {
const listContainer = document.getElementById("restoreFilesList"); const listContainer = document.getElementById("restoreFilesList");
@@ -271,7 +271,7 @@ export function loadTrashItems() {
* Automatically purges (permanently deletes) trash items older than 3 days. * Automatically purges (permanently deletes) trash items older than 3 days.
*/ */
function autoPurgeOldTrash() { function autoPurgeOldTrash() {
fetch("api/file/getTrashItems.php", { credentials: "include" }) fetch("/api/file/getTrashItems.php", { credentials: "include" })
.then(response => response.json()) .then(response => response.json())
.then(trashItems => { .then(trashItems => {
const now = Date.now(); const now = Date.now();
@@ -279,7 +279,7 @@ function autoPurgeOldTrash() {
const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays); const oldItems = trashItems.filter(item => (now - (item.trashedAt * 1000)) > threeDays);
if (oldItems.length > 0) { if (oldItems.length > 0) {
const files = oldItems.map(item => item.trashName); const files = oldItems.map(item => item.trashName);
fetch("api/file/deleteTrashFiles.php", { fetch("/api/file/deleteTrashFiles.php", {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { headers: {

View File

@@ -126,7 +126,7 @@ function removeChunkFolderRepeatedly(identifier, csrfToken, maxAttempts = 3, int
// Prefix with "resumable_" to match your PHP regex. // Prefix with "resumable_" to match your PHP regex.
params.append('folder', 'resumable_' + identifier); params.append('folder', 'resumable_' + identifier);
params.append('csrf_token', csrfToken); params.append('csrf_token', csrfToken);
fetch('api/upload/removeChunks.php', { fetch('/api/upload/removeChunks.php', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 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.setRequestHeader("X-CSRF-Token", window.csrfToken);
xhr.send(formData); xhr.send(formData);
}); });

View File

@@ -238,22 +238,39 @@ class AuthController
$token = bin2hex(random_bytes(32)); $token = bin2hex(random_bytes(32));
$expiry = time() + 30 * 24 * 60 * 60; $expiry = time() + 30 * 24 * 60 * 60;
$all = []; $all = [];
if (file_exists($tokFile)) { if (file_exists($tokFile)) {
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']); $dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
$all = json_decode($dec, true) ?: []; $all = json_decode($dec, true) ?: [];
} }
$all[$token] = [ $all[$token] = [
'username' => $username, 'username' => $username,
'expiry' => $expiry, 'expiry' => $expiry,
'isAdmin' => $_SESSION['isAdmin'] 'isAdmin' => $_SESSION['isAdmin']
]; ];
file_put_contents( file_put_contents(
$tokFile, $tokFile,
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']), encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
LOCK_EX 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('remember_me_token', $token, $expiry, '/', '', $secure, true);
setcookie(
session_name(),
session_id(),
$expiry,
'/',
'',
$secure,
true
);
session_regenerate_id(true);
} }
echo json_encode([ echo json_encode([

View File

@@ -847,104 +847,147 @@ class UserController
* ) * )
*/ */
public function verifyTOTP() public function verifyTOTP()
{ {
header('Content-Type: application/json'); header('Content-Type: application/json');
// Set CSP headers if desired: header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
// Ratelimit
// Ratelimit: initialize totp_failures if not set. if (!isset($_SESSION['totp_failures'])) {
if (!isset($_SESSION['totp_failures'])) { $_SESSION['totp_failures'] = 0;
$_SESSION['totp_failures'] = 0; }
} if ($_SESSION['totp_failures'] >= 5) {
if ($_SESSION['totp_failures'] >= 5) { http_response_code(429);
http_response_code(429); echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']);
echo json_encode(['status' => 'error', 'message' => 'Too many TOTP attempts. Please try again later.']); exit;
exit; }
}
// Must be authenticated OR pending login
// Must be authenticated OR have a pending login. if (!((!empty($_SESSION['authenticated'])) || isset($_SESSION['pending_login_user']))) {
if (!((isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) || isset($_SESSION['pending_login_user']))) { http_response_code(403);
http_response_code(403); echo json_encode(['status' => 'error', 'message' => 'Not authenticated']);
echo json_encode(['status' => 'error', 'message' => 'Not authenticated']); exit;
exit; }
}
// CSRF check
// CSRF check. $headersArr = array_change_key_case(getallheaders(), CASE_LOWER);
$headersArr = array_change_key_case(getallheaders(), CASE_LOWER); $csrfHeader = $headersArr['x-csrf-token'] ?? '';
$csrfHeader = isset($headersArr['x-csrf-token']) ? trim($headersArr['x-csrf-token']) : ''; if (empty($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) { http_response_code(403);
http_response_code(403); echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']);
echo json_encode(['status' => 'error', 'message' => 'Invalid CSRF token']); exit;
exit; }
}
// Parse and validate input
// Parse input. $inputData = json_decode(file_get_contents("php://input"), true);
$inputData = json_decode(file_get_contents("php://input"), true); $code = trim($inputData['totp_code'] ?? '');
$code = trim($inputData['totp_code'] ?? ''); if (!preg_match('/^\d{6}$/', $code)) {
if (!preg_match('/^\d{6}$/', $code)) { http_response_code(400);
http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']);
echo json_encode(['status' => 'error', 'message' => 'A valid 6-digit TOTP code is required']); exit;
exit; }
}
// TFA helper
// Create TFA object. $tfa = new \RobThree\Auth\TwoFactorAuth(
$tfa = new \RobThree\Auth\TwoFactorAuth( new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(),
new \RobThree\Auth\Providers\Qr\GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, \RobThree\Auth\Algorithm::Sha1
'FileRise', );
6,
30, // Pendinglogin flow (first password step passed)
\RobThree\Auth\Algorithm::Sha1 if (isset($_SESSION['pending_login_user'])) {
); $username = $_SESSION['pending_login_user'];
$pendingSecret = $_SESSION['pending_login_secret'] ?? null;
// Check if we are in pending login flow. $rememberMe = $_SESSION['pending_login_remember_me'] ?? false;
if (isset($_SESSION['pending_login_user'])) {
$username = $_SESSION['pending_login_user']; if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) {
$pendingSecret = $_SESSION['pending_login_secret'] ?? null; $_SESSION['totp_failures']++;
if (!$pendingSecret || !$tfa->verifyCode($pendingSecret, $code)) { http_response_code(400);
$_SESSION['totp_failures']++; echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']);
http_response_code(400); exit;
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); }
exit;
} // === Issue “remember me” token if requested ===
// Successful pending login: finalize login. if ($rememberMe) {
session_regenerate_id(true); $tokFile = USERS_DIR . 'persistent_tokens.json';
$_SESSION['authenticated'] = true; $token = bin2hex(random_bytes(32));
$_SESSION['username'] = $username; $expiry = time() + 30 * 24 * 60 * 60;
// Set isAdmin based on user role. $all = [];
$_SESSION['isAdmin'] = (userModel::getUserRole($username) === "1");
// Load additional permissions (e.g., folderOnly) as needed. if (file_exists($tokFile)) {
$_SESSION['folderOnly'] = loadUserPermissions($username); $dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']); $all = json_decode($dec, true) ?: [];
echo json_encode(['status' => 'ok', 'message' => 'Login successful']); }
exit; $all[$token] = [
} 'username' => $username,
'expiry' => $expiry,
// Otherwise, we are in setup/verification flow. 'isAdmin' => $_SESSION['isAdmin']
$username = $_SESSION['username'] ?? ''; ];
if (!$username) { file_put_contents(
http_response_code(400); $tokFile,
echo json_encode(['status' => 'error', 'message' => 'Username not found in session']); encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
exit; LOCK_EX
} );
// Retrieve the user's TOTP secret from the model. $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
$totpSecret = userModel::getTOTPSecret($username);
if (!$totpSecret) { // Persistent cookie
http_response_code(500); setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
echo json_encode(['status' => 'error', 'message' => 'TOTP secret not found. Please set up TOTP again.']);
exit; // Reissue PHP session cookie
} setcookie(
session_name(),
if (!$tfa->verifyCode($totpSecret, $code)) { session_id(),
$_SESSION['totp_failures']++; $expiry,
http_response_code(400); '/',
echo json_encode(['status' => 'error', 'message' => 'Invalid TOTP code']); '',
exit; $secure,
} true
);
// Successful verification. }
unset($_SESSION['totp_failures']);
echo json_encode(['status' => 'ok', 'message' => 'TOTP successfully verified']); // 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']);
}
} }