Fetch URL fixes, Extended “Remember Me” cookie behavior, submitLogin() overhaul
This commit is contained in:
32
CHANGELOG.md
32
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
|
||||
|
||||
@@ -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 = '<i class="material-icons">account_circle</i>';
|
||||
|
||||
|
||||
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 },
|
||||
|
||||
@@ -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")} <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||
|
||||
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 || []);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -196,7 +196,7 @@ export function loadFileList(folderParam) {
|
||||
fileListContainer.style.visibility = "hidden";
|
||||
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 => {
|
||||
if (response.status === 401) {
|
||||
showToast("Session expired. Please log in again.");
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user