diff --git a/.htaccess b/.htaccess
index a9531d2..dd58d5e 100644
--- a/.htaccess
+++ b/.htaccess
@@ -4,26 +4,32 @@
Options -Indexes
# -----------------------------
-# 2) Default index files
+# Default index files
# -----------------------------
DirectoryIndex index.html
# -----------------------------
-# 3) Deny access to hidden files
+# Deny access to hidden files
# -----------------------------
-# (blocks access to .htaccess, .gitignore, etc.)
Require all denied
# -----------------------------
-# 4) Enforce HTTPS (optional)
+# Enforce HTTPS (optional)
# -----------------------------
-# Uncomment if you have SSL configured
-#RewriteEngine On
+RewriteEngine On
#RewriteCond %{HTTPS} off
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
+ # Allow requests from a specific origin
+ #Header set Access-Control-Allow-Origin "https://demo.filerise.net"
+ Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
+ Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-CSRF-Token"
+ Header set Access-Control-Allow-Credentials "true"
+
+
# Prevent clickjacking
Header always set X-Frame-Options "SAMEORIGIN"
@@ -40,9 +46,30 @@ DirectoryIndex index.html
Header set Pragma "no-cache"
Header set Expires "0"
-
# JS/CSS: short‑term cache, revalidate regularly
Header set Cache-Control "public, max-age=3600, must-revalidate"
-
\ No newline at end of file
+
+
+# -----------------------------
+# Additional Security Headers
+# -----------------------------
+
+ # Enforce HTTPS for a year with subdomains and preload option.
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+ # Set a Referrer Policy.
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
+ # Permissions Policy: disable features you don't need.
+ Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
+ # IE-specific header to prevent downloads from opening in IE.
+ Header always set X-Download-Options "noopen"
+ # Expect-CT header for Certificate Transparency (optional).
+ Header always set Expect-CT "max-age=86400, enforce"
+
+
+# -----------------------------
+# Disable TRACE method
+# -----------------------------
+RewriteCond %{REQUEST_METHOD} ^TRACE
+RewriteRule .* - [F]
\ No newline at end of file
diff --git a/auth.php b/auth.php
index 972b736..d6c9d45 100644
--- a/auth.php
+++ b/auth.php
@@ -1,16 +1,25 @@
getMessage());
+ http_response_code(500);
+ echo json_encode(["error" => "Internal Server Error"]);
+ exit();
+});
+
/**
* Helper: Get the user's role from users.txt.
*/
function getUserRole($username) {
$usersFile = USERS_DIR . USERS_FILE;
if (file_exists($usersFile)) {
- $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
- foreach ($lines as $line) {
+ foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$parts = explode(":", trim($line));
if (count($parts) >= 3 && $parts[0] === $username) {
return trim($parts[2]);
@@ -21,37 +30,25 @@ function getUserRole($username) {
}
/* --- OIDC Authentication Flow --- */
-if (isset($_GET['oidc'])) {
- // Read and decrypt OIDC configuration from JSON file.
+// Detect either ?oidc=… or a callback that only has ?code=
+$oidcAction = $_GET['oidc'] ?? null;
+if (!$oidcAction && isset($_GET['code'])) {
+ $oidcAction = 'callback';
+}
+if ($oidcAction) {
$adminConfigFile = USERS_DIR . 'adminConfig.json';
if (file_exists($adminConfigFile)) {
- $encryptedContent = file_get_contents($adminConfigFile);
- $decryptedContent = decryptData($encryptedContent, $encryptionKey);
- if ($decryptedContent === false) {
- // Log internal error and return a generic message.
- error_log("Failed to decrypt admin configuration.");
- echo json_encode(['error' => 'Internal error.']);
- exit;
- }
- $adminConfig = json_decode($decryptedContent, true);
- if (isset($adminConfig['oidc'])) {
- $oidcConfig = $adminConfig['oidc'];
- $oidc_provider_url = !empty($oidcConfig['providerUrl']) ? $oidcConfig['providerUrl'] : 'https://your-oidc-provider.com';
- $oidc_client_id = !empty($oidcConfig['clientId']) ? $oidcConfig['clientId'] : 'YOUR_CLIENT_ID';
- $oidc_client_secret = !empty($oidcConfig['clientSecret']) ? $oidcConfig['clientSecret'] : 'YOUR_CLIENT_SECRET';
- $oidc_redirect_uri = !empty($oidcConfig['redirectUri']) ? $oidcConfig['redirectUri'] : 'https://yourdomain.com/auth.php?oidc=callback';
- } else {
- $oidc_provider_url = 'https://your-oidc-provider.com';
- $oidc_client_id = 'YOUR_CLIENT_ID';
- $oidc_client_secret = 'YOUR_CLIENT_SECRET';
- $oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
- }
+ $enc = file_get_contents($adminConfigFile);
+ $dec = decryptData($enc, $encryptionKey);
+ $cfg = $dec !== false ? json_decode($dec, true) : [];
} else {
- $oidc_provider_url = 'https://your-oidc-provider.com';
- $oidc_client_id = 'YOUR_CLIENT_ID';
- $oidc_client_secret = 'YOUR_CLIENT_SECRET';
- $oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
+ $cfg = [];
}
+ $oidc_provider_url = $cfg['oidc']['providerUrl'] ?? 'https://your-oidc-provider.com';
+ $oidc_client_id = $cfg['oidc']['clientId'] ?? 'YOUR_CLIENT_ID';
+ $oidc_client_secret = $cfg['oidc']['clientSecret'] ?? 'YOUR_CLIENT_SECRET';
+ // Use your production domain for redirect URI.
+ $oidc_redirect_uri = $cfg['oidc']['redirectUri'] ?? 'https://yourdomain.com/auth.php?oidc=callback';
$oidc = new Jumbojett\OpenIDConnectClient(
$oidc_provider_url,
@@ -60,31 +57,54 @@ if (isset($_GET['oidc'])) {
);
$oidc->setRedirectURL($oidc_redirect_uri);
- if ($_GET['oidc'] === 'callback') {
+ if ($oidcAction === 'callback') {
try {
$oidc->authenticate();
$username = $oidc->requestUserInfo('preferred_username');
+
+ // Check if this user has a TOTP secret.
+ $usersFile = USERS_DIR . USERS_FILE;
+ $totp_secret = null;
+ if (file_exists($usersFile)) {
+ foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
+ $parts = explode(":", trim($line));
+ if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
+ $totp_secret = decryptData($parts[3], $encryptionKey);
+ break;
+ }
+ }
+ }
+ if ($totp_secret) {
+ // Hold pending login & prompt for TOTP.
+ $_SESSION['pending_login_user'] = $username;
+ $_SESSION['pending_login_secret'] = $totp_secret;
+ header("Location: index.html?totp_required=1");
+ exit();
+ }
+
+ // No TOTP → finalize login.
session_regenerate_id(true);
$_SESSION["authenticated"] = true;
- $_SESSION["username"] = $username;
- // Determine the user role from users.txt.
- $userRole = getUserRole($username);
- $_SESSION["isAdmin"] = ($userRole === "1");
- // *** Use loadUserPermissions() here instead of loadFolderPermission() ***
- $_SESSION["folderOnly"] = loadUserPermissions($username);
+ $_SESSION["username"] = $username;
+ $_SESSION["isAdmin"] = (getUserRole($username) === "1");
+ $_SESSION["folderOnly"] = loadUserPermissions($username);
+
header("Location: index.html");
exit();
} catch (Exception $e) {
error_log("OIDC authentication error: " . $e->getMessage());
+ http_response_code(401);
echo json_encode(["error" => "Authentication failed."]);
exit();
}
} else {
+ // Initiate OIDC authentication.
try {
$oidc->authenticate();
exit();
} catch (Exception $e) {
error_log("OIDC initiation error: " . $e->getMessage());
+ http_response_code(401);
echo json_encode(["error" => "Authentication initiation failed."]);
exit();
}
@@ -92,10 +112,9 @@ if (isset($_GET['oidc'])) {
}
/* --- Fallback: Form-based Authentication --- */
-// (Form-based branch code remains unchanged. It calls loadUserPermissions() in its basic auth branch.)
$usersFile = USERS_DIR . USERS_FILE;
$maxAttempts = 5;
-$lockoutTime = 30 * 60;
+$lockoutTime = 30 * 60; // 30 minutes
$attemptsFile = USERS_DIR . 'failed_logins.json';
$failedLogFile = USERS_DIR . 'failed_login.log';
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
@@ -111,7 +130,7 @@ function loadFailedAttempts($file) {
}
function saveFailedAttempts($file, $data) {
- file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
+ file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX);
}
$ip = $_SERVER['REMOTE_ADDR'];
@@ -121,6 +140,7 @@ $failedAttempts = loadFailedAttempts($attemptsFile);
if (isset($failedAttempts[$ip])) {
$attemptData = $failedAttempts[$ip];
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
+ http_response_code(429);
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
exit();
}
@@ -137,11 +157,9 @@ function authenticate($username, $password) {
if (count($parts) < 3) continue;
if ($username === $parts[0] && password_verify($password, $parts[1])) {
$result = ['role' => $parts[2]];
- if (isset($parts[3]) && !empty($parts[3])) {
- $result['totp_secret'] = decryptData($parts[3], $encryptionKey);
- } else {
- $result['totp_secret'] = null;
- }
+ $result['totp_secret'] = (isset($parts[3]) && !empty($parts[3]))
+ ? decryptData($parts[3], $encryptionKey)
+ : null;
return $result;
}
}
@@ -154,11 +172,13 @@ $password = trim($data["password"] ?? "");
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
if (!$username || !$password) {
+ http_response_code(400);
echo json_encode(["error" => "Username and password are required"]);
exit();
}
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
+ http_response_code(400);
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
exit();
}
@@ -167,6 +187,7 @@ $user = authenticate($username, $password);
if ($user !== false) {
if (!empty($user['totp_secret'])) {
if (empty($data['totp_code'])) {
+ http_response_code(401);
echo json_encode([
"totp_required" => true,
"message" => "TOTP code required"
@@ -176,6 +197,7 @@ if ($user !== false) {
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
$providedCode = trim($data['totp_code']);
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
+ http_response_code(401);
echo json_encode(["error" => "Invalid TOTP code"]);
exit();
}
@@ -229,6 +251,7 @@ if ($user !== false) {
saveFailedAttempts($attemptsFile, $failedAttempts);
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
+ http_response_code(401);
echo json_encode(["error" => "Invalid credentials"]);
}
?>
\ No newline at end of file
diff --git a/js/auth.js b/js/auth.js
index 4dfb593..47cbe58 100644
--- a/js/auth.js
+++ b/js/auth.js
@@ -1,11 +1,16 @@
import { sendRequest } from './networkUtils.js';
-import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
+import {
+ toggleVisibility,
+ showToast as originalShowToast,
+ attachEnterKeyListener,
+ showCustomConfirmModal
+} from './domUtils.js';
import { loadFileList } from './fileListView.js';
import { initFileActions } from './fileActions.js';
import { renderFileTable } from './fileListView.js';
import { loadFolderTree } from './folderManager.js';
import {
- openTOTPLoginModal,
+ openTOTPLoginModal as originalOpenTOTPLoginModal,
openUserPanel,
openTOTPModal,
closeTOTPModal,
@@ -24,6 +29,43 @@ const currentOIDCConfig = {
};
window.currentOIDCConfig = currentOIDCConfig;
+/* ----------------- TOTP & Toast Overrides ----------------- */
+// detect if we’re in a pending‑TOTP state
+window.pendingTOTP = new URLSearchParams(window.location.search).get('totp_required') === '1';
+
+// override showToast to suppress the "Please log in to continue." toast during TOTP
+function showToast(msg) {
+ if (window.pendingTOTP && msg === "Please log in to continue.") {
+ return;
+ }
+ originalShowToast(msg);
+}
+window.showToast = showToast;
+
+// wrap the TOTP modal opener to disable other login buttons only for Basic/OIDC flows
+function openTOTPLoginModal() {
+ originalOpenTOTPLoginModal();
+
+ const isFormLogin = Boolean(window.__lastLoginData);
+ if (!isFormLogin) {
+ // disable Basic‑Auth link
+ const basicLink = document.querySelector("a[href='login_basic.php']");
+ if (basicLink) {
+ basicLink.style.pointerEvents = 'none';
+ basicLink.style.opacity = '0.5';
+ }
+ // disable OIDC button
+ const oidcBtn = document.getElementById("oidcLoginBtn");
+ if (oidcBtn) {
+ oidcBtn.disabled = true;
+ oidcBtn.style.opacity = '0.5';
+ }
+ // hide the form login
+ const authForm = document.getElementById("authForm");
+ if (authForm) authForm.style.display = 'none';
+ }
+}
+
/* ----------------- Utility Functions ----------------- */
function updateItemsPerPageSelect() {
const selectElem = document.querySelector(".form-control.bottom-select");
@@ -85,7 +127,6 @@ function updateAuthenticatedUI(data) {
if (typeof data.totp_enabled !== "undefined") {
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
}
-
if (data.username) {
localStorage.setItem("username", data.username);
}
@@ -103,11 +144,8 @@ function updateAuthenticatedUI(data) {
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
restoreBtn.innerHTML = 'restore_from_trash';
- if (firstButton) {
- insertAfter(restoreBtn, firstButton);
- } else {
- headerButtons.appendChild(restoreBtn);
- }
+ if (firstButton) insertAfter(restoreBtn, firstButton);
+ else headerButtons.appendChild(restoreBtn);
}
restoreBtn.style.display = "block";
@@ -128,7 +166,7 @@ function updateAuthenticatedUI(data) {
const adminPanelBtn = document.getElementById("adminPanelBtn");
if (adminPanelBtn) adminPanelBtn.style.display = "none";
}
-
+
if (window.location.hostname !== "demo.filerise.net") {
let userPanelBtn = document.getElementById("userPanelBtn");
if (!userPanelBtn) {
@@ -136,17 +174,10 @@ function updateAuthenticatedUI(data) {
userPanelBtn.id = "userPanelBtn";
userPanelBtn.classList.add("btn", "btn-user");
userPanelBtn.innerHTML = 'account_circle';
- let adminPanelBtn = document.getElementById("adminPanelBtn");
- if (adminPanelBtn) {
- insertAfter(userPanelBtn, adminPanelBtn);
- } else {
- const firstButton = headerButtons.firstElementChild;
- if (firstButton) {
- insertAfter(userPanelBtn, firstButton);
- } else {
- headerButtons.appendChild(userPanelBtn);
- }
- }
+ const adminBtn = document.getElementById("adminPanelBtn");
+ if (adminBtn) insertAfter(userPanelBtn, adminBtn);
+ else if (firstButton) insertAfter(userPanelBtn, firstButton);
+ else headerButtons.appendChild(userPanelBtn);
userPanelBtn.addEventListener("click", openUserPanel);
} else {
userPanelBtn.style.display = "block";
@@ -193,6 +224,7 @@ function checkAuthentication(showLoginToast = true) {
/* ----------------- Authentication Submission ----------------- */
function submitLogin(data) {
setLastLoginData(data);
+ window.__lastLoginData = data;
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
.then(response => {
if (response.success) {
@@ -220,7 +252,7 @@ function submitLogin(data) {
}
window.submitLogin = submitLogin;
-/* ----------------- Other Helpers and Initialization ----------------- */
+/* ----------------- Other Helpers ----------------- */
window.changeItemsPerPage = function (value) {
localStorage.setItem("itemsPerPage", value);
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
@@ -259,7 +291,7 @@ function loadUserList() {
closeRemoveUserModal();
}
})
- .catch(() => { });
+ .catch(() => {});
}
window.loadUserList = loadUserList;
@@ -286,7 +318,7 @@ function initAuth() {
method: "POST",
credentials: "include",
headers: { "X-CSRF-Token": window.csrfToken }
- }).then(() => window.location.reload(true)).catch(() => { });
+ }).then(() => window.location.reload(true)).catch(() => {});
});
document.getElementById("addUserBtn").addEventListener("click", function () {
resetUserForm();
@@ -352,7 +384,7 @@ function initAuth() {
showToast("Error: " + (data.error || "Could not remove user"));
}
})
- .catch(() => { });
+ .catch(() => {});
});
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
document.getElementById("changePasswordBtn").addEventListener("click", function () {
@@ -404,13 +436,19 @@ document.addEventListener("DOMContentLoaded", function () {
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
});
+
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
if (oidcLoginBtn) {
oidcLoginBtn.addEventListener("click", () => {
- // Redirect to the OIDC auth endpoint. The endpoint can be adjusted if needed.
window.location.href = "auth.php?oidc=initiate";
});
}
+
+ // If TOTP is pending, show modal and skip normal auth init
+ if (window.pendingTOTP) {
+ openTOTPLoginModal();
+ return;
+ }
});
export { initAuth, checkAuthentication };
\ No newline at end of file
diff --git a/js/authModals.js b/js/authModals.js
index b143278..78bbd8a 100644
--- a/js/authModals.js
+++ b/js/authModals.js
@@ -3,61 +3,102 @@ import { sendRequest } from './networkUtils.js';
const version = "v1.0.7";
const adminTitle = `Admin Panel ${version}`;
-let lastLoginData = null;
+let lastLoginData = null;
export function setLastLoginData(data) {
- lastLoginData = data;
+ lastLoginData = data;
+ // expose to auth.js so it can tell form-login vs basic/oidc
+ window.__lastLoginData = data;
}
export function openTOTPLoginModal() {
- let totpLoginModal = document.getElementById("totpLoginModal");
- const isDarkMode = document.body.classList.contains("dark-mode");
- const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
- const textColor = isDarkMode ? "#e0e0e0" : "#000";
+ let totpLoginModal = document.getElementById("totpLoginModal");
+ const isDarkMode = document.body.classList.contains("dark-mode");
+ const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
+ const textColor = isDarkMode ? "#e0e0e0" : "#000";
- if (!totpLoginModal) {
- totpLoginModal = document.createElement("div");
- totpLoginModal.id = "totpLoginModal";
- totpLoginModal.style.cssText = `
+ if (!totpLoginModal) {
+ totpLoginModal = document.createElement("div");
+ totpLoginModal.id = "totpLoginModal";
+ totpLoginModal.style.cssText = `
position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
+ top: 0; left: 0;
+ width: 100vw; height: 100vh;
background-color: rgba(0,0,0,0.5);
- display: flex;
- justify-content: center;
- align-items: center;
+ display: flex; justify-content: center; align-items: center;
z-index: 3200;
`;
- totpLoginModal.innerHTML = `
-
-
×
+ totpLoginModal.innerHTML = `
+
+ ×
Enter TOTP Code
-
+
`;
- document.body.appendChild(totpLoginModal);
- document.getElementById("closeTOTPLoginModal").addEventListener("click", () => {
- totpLoginModal.style.display = "none";
- });
- const totpInput = document.getElementById("totpLoginInput");
- totpInput.focus();
- totpInput.addEventListener("input", function () {
- if (this.value.trim().length === 6 && lastLoginData) {
- lastLoginData.totp_code = this.value.trim();
- totpLoginModal.style.display = "none";
- if (typeof window.submitLogin === "function") {
- window.submitLogin(lastLoginData);
- }
- }
- });
- } else {
- totpLoginModal.style.display = "flex";
- const modalContent = totpLoginModal.firstElementChild;
- modalContent.style.background = modalBg;
- modalContent.style.color = textColor;
+ document.body.appendChild(totpLoginModal);
+
+ document.getElementById("closeTOTPLoginModal").addEventListener("click", () => {
+ totpLoginModal.style.display = "none";
+ });
+
+ const totpInput = document.getElementById("totpLoginInput");
+ totpInput.focus();
+
+ totpInput.addEventListener("input", function () {
+ const code = this.value.trim();
+ if (code.length === 6) {
+ // FORM-BASED LOGIN
+ if (lastLoginData) {
+ totpLoginModal.style.display = "none";
+ lastLoginData.totp_code = code;
+ window.submitLogin(lastLoginData);
+
+ // BASIC-AUTH / OIDC LOGIN
+ } else {
+ // keep modal open until we know the result
+ fetch("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.success) {
+ window.location.href = "index.html";
+ } else {
+ showToast(json.error || json.message || "TOTP verification failed");
+ this.value = "";
+ totpLoginModal.style.display = "flex";
+ totpInput.focus();
+ }
+ })
+ .catch(() => {
+ showToast("TOTP verification failed");
+ this.value = "";
+ totpLoginModal.style.display = "flex";
+ totpInput.focus();
+ });
+ }
+ }
+ });
+ } else {
+ totpLoginModal.style.display = "flex";
+ const modalContent = totpLoginModal.firstElementChild;
+ modalContent.style.background = modalBg;
+ modalContent.style.color = textColor;
+ // reset input if reopening
+ const totpInput = document.getElementById("totpLoginInput");
+ if (totpInput) {
+ totpInput.value = "";
+ totpInput.focus();
}
+ }
}
export function openUserPanel() {
diff --git a/js/fileListView.js b/js/fileListView.js
index 89c41c9..4672202 100644
--- a/js/fileListView.js
+++ b/js/fileListView.js
@@ -297,7 +297,7 @@ export function renderGalleryView(folder) {
-
@@ -306,10 +306,23 @@ export function renderGalleryView(folder) {
});
galleryHTML += "";
+
fileListContainer.innerHTML = galleryHTML;
createViewToggleButton();
updateFileActionButtons();
+
+ // Bind share button clicks
+ document.querySelectorAll(".share-btn").forEach(btn => {
+ btn.addEventListener("click", e => {
+ e.stopPropagation();
+ const fileName = btn.getAttribute("data-file");
+ const file = fileData.find(f => f.name === fileName);
+ import('./filePreview.js').then(module => {
+ module.openShareModal(file, folder);
+ });
+ });
+ });
}
export function sortFiles(column, folder) {
diff --git a/js/filePreview.js b/js/filePreview.js
index 07325cb..65f425a 100644
--- a/js/filePreview.js
+++ b/js/filePreview.js
@@ -253,4 +253,5 @@ export function displayFilePreview(file, container) {
}
}
-window.previewFile = previewFile;
\ No newline at end of file
+window.previewFile = previewFile;
+window.openShareModal = openShareModal;
\ No newline at end of file
diff --git a/login_basic.php b/login_basic.php
index 389e146..f2b938f 100644
--- a/login_basic.php
+++ b/login_basic.php
@@ -3,6 +3,19 @@ require_once 'config.php';
$usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined
+// Helper: retrieve a user's TOTP secret from users.txt
+function getUserTOTPSecret($username) {
+ global $encryptionKey, $usersFile;
+ if (!file_exists($usersFile)) return null;
+ foreach (file($usersFile, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
+ $parts = explode(':', trim($line));
+ if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
+ return decryptData($parts[3], $encryptionKey);
+ }
+ }
+ return null;
+}
+
// Reuse the same authentication function
function authenticate($username, $password)
{
@@ -43,15 +56,9 @@ function loadFolderPermission($username) {
$permissionsFile = USERS_DIR . 'userPermissions.json';
if (file_exists($permissionsFile)) {
$content = file_get_contents($permissionsFile);
- // Try to decrypt the content.
- $decryptedContent = decryptData($content, $encryptionKey);
- if ($decryptedContent !== false) {
- $permissions = json_decode($decryptedContent, true);
- } else {
- $permissions = json_decode($content, true);
- }
+ $decrypted = decryptData($content, $encryptionKey);
+ $permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true);
if (is_array($permissions)) {
- // Use case-insensitive comparison.
foreach ($permissions as $storedUsername => $data) {
if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) {
return (bool)$data['folderOnly'];
@@ -59,7 +66,7 @@ function loadFolderPermission($username) {
}
}
}
- return false; // Default if not set.
+ return false;
}
// Check if the user has sent HTTP Basic auth credentials.
@@ -68,39 +75,46 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('HTTP/1.0 401 Unauthorized');
echo 'Authorization Required';
exit;
-} else {
- $username = trim($_SERVER['PHP_AUTH_USER']);
- $password = trim($_SERVER['PHP_AUTH_PW']);
-
- // Validate username format (optional)
- if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
- header('WWW-Authenticate: Basic realm="FileRise Login"');
- header('HTTP/1.0 401 Unauthorized');
- echo 'Invalid username format';
- exit;
- }
-
- // Attempt authentication
- $roleFromAuth = authenticate($username, $password);
- if ($roleFromAuth !== false) {
- // Use getUserRole() to determine the user's role from the file
- $actualRole = getUserRole($username);
- session_regenerate_id(true);
- $_SESSION["authenticated"] = true;
- $_SESSION["username"] = $username;
- $_SESSION["isAdmin"] = ($actualRole === "1");
- // Set the folderOnly flag based on userPermissions.json.
- $_SESSION["folderOnly"] = loadFolderPermission($username);
-
- // Redirect to the main page (or output JSON for testing)
- header("Location: index.html");
- exit;
- } else {
- // Invalid credentials; prompt again
- header('WWW-Authenticate: Basic realm="FileRise Login"');
- header('HTTP/1.0 401 Unauthorized');
- echo 'Invalid credentials';
- exit;
- }
}
+
+$username = trim($_SERVER['PHP_AUTH_USER']);
+$password = trim($_SERVER['PHP_AUTH_PW']);
+
+// Validate username format (optional)
+if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
+ header('WWW-Authenticate: Basic realm="FileRise Login"');
+ header('HTTP/1.0 401 Unauthorized');
+ echo 'Invalid username format';
+ exit;
+}
+
+// Attempt authentication
+$roleFromAuth = authenticate($username, $password);
+if ($roleFromAuth !== false) {
+ // --- NEW: check for TOTP secret ---
+ $secret = getUserTOTPSecret($username);
+ if ($secret) {
+ // hold user & secret in session and ask client for TOTP
+ $_SESSION['pending_login_user'] = $username;
+ $_SESSION['pending_login_secret'] = $secret;
+ header("Location: index.html?totp_required=1");
+ exit;
+ }
+
+ // no TOTP, proceed as before
+ session_regenerate_id(true);
+ $_SESSION["authenticated"] = true;
+ $_SESSION["username"] = $username;
+ $_SESSION["isAdmin"] = (getUserRole($username) === "1");
+ $_SESSION["folderOnly"] = loadFolderPermission($username);
+
+ header("Location: index.html");
+ exit;
+}
+
+// Invalid credentials; prompt again
+header('WWW-Authenticate: Basic realm="FileRise Login"');
+header('HTTP/1.0 401 Unauthorized');
+echo 'Invalid credentials';
+exit;
?>
\ No newline at end of file
diff --git a/totp_verify.php b/totp_verify.php
index 532aae6..96d6322 100644
--- a/totp_verify.php
+++ b/totp_verify.php
@@ -1,84 +1,153 @@
"Not authenticated"]);
- exit;
+// Secure session cookie
+session_set_cookie_params([
+ 'lifetime' => 0,
+ 'path' => '/',
+ 'domain' => '', // your domain
+ 'secure' => true, // only over HTTPS
+ 'httponly' => true,
+ 'samesite' => 'Lax'
+]);
+if (session_status() !== PHP_SESSION_ACTIVE) {
+ session_start();
}
-// Verify CSRF token from request headers.
-$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
-if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
- http_response_code(403);
- echo json_encode(["error" => "Invalid CSRF token"]);
- exit;
-}
-
-// Ensure Content-Type is JSON.
+// JSON + CSP
header('Content-Type: application/json');
+header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';");
-// Read and decode the JSON request body.
-$input = json_decode(file_get_contents("php://input"), true);
-if (!isset($input['totp_code']) || strlen(trim($input['totp_code'])) !== 6 || !ctype_digit(trim($input['totp_code']))) {
- http_response_code(400);
- echo json_encode(["error" => "A valid 6-digit TOTP code is required"]);
- exit;
-}
+try {
+ // standardized error helper
+ function respond($status, $code, $message, $data = []) {
+ http_response_code($code);
+ echo json_encode([
+ 'status' => $status,
+ 'code' => $code,
+ 'message' => $message,
+ 'data' => $data
+ ]);
+ exit;
+ }
-$totpCode = trim($input['totp_code']);
-$username = $_SESSION['username'] ?? '';
-if (empty($username)) {
- http_response_code(400);
- echo json_encode(["error" => "Username not found in session"]);
- exit;
-}
+ // Rate‑limit TOTP attempts
+ if (!isset($_SESSION['totp_failures'])) {
+ $_SESSION['totp_failures'] = 0;
+ }
+ if ($_SESSION['totp_failures'] >= 5) {
+ respond('error', 429, 'Too many TOTP attempts. Please try again later.');
+ }
-/**
- * Retrieves the current user's TOTP secret from users.txt.
- *
- * @param string $username
- * @return string|null The decrypted TOTP secret or null if not found.
- */
-function getUserTOTPSecret($username) {
- global $encryptionKey;
- // Define the path to your users file.
- $usersFile = USERS_DIR . USERS_FILE;
- if (!file_exists($usersFile)) {
+ /**
+ * Helper: Get a user's role from users.txt
+ */
+ function getUserRole(string $username): ?string {
+ $usersFile = USERS_DIR . USERS_FILE;
+ if (!file_exists($usersFile)) return null;
+ foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
+ $parts = explode(':', trim($line));
+ if (count($parts) >= 3 && $parts[0] === $username) {
+ return trim($parts[2]);
+ }
+ }
return null;
}
- $lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
- foreach ($lines as $line) {
- $parts = explode(':', trim($line));
- // Assuming format: username:hashedPassword:role:encryptedTOTPSecret
- if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
- return decryptData($parts[3], $encryptionKey);
- }
+
+ // Must be authenticated or pending TOTP
+ if (
+ !(
+ (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true)
+ || isset($_SESSION['pending_login_user'])
+ )
+ ) {
+ respond('error', 403, 'Not authenticated');
}
- return null;
-}
-// Retrieve the user's TOTP secret.
-$totpSecret = getUserTOTPSecret($username);
-if (!$totpSecret) {
+ // CSRF check
+ $csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
+ if (!isset($_SESSION['csrf_token']) || $csrfHeader !== $_SESSION['csrf_token']) {
+ respond('error', 403, 'Invalid CSRF token');
+ }
+
+ // Parse & validate input
+ $input = json_decode(file_get_contents("php://input"), true);
+ $code = trim($input['totp_code'] ?? '');
+ if (!preg_match('/^\d{6}$/', $code)) {
+ respond('error', 400, 'A valid 6-digit TOTP code is required');
+ }
+
+ // LOGIN flow (Basic‑Auth or OIDC)
+ if (isset($_SESSION['pending_login_user'])) {
+ $username = $_SESSION['pending_login_user'];
+ $totpSecret = $_SESSION['pending_login_secret'];
+ $tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
+
+ if (!$tfa->verifyCode($totpSecret, $code)) {
+ $_SESSION['totp_failures']++;
+ respond('error', 400, 'Invalid TOTP code');
+ }
+
+ // success → complete login
+ session_regenerate_id(true);
+ $_SESSION['authenticated'] = true;
+ $_SESSION['username'] = $username;
+ $_SESSION['isAdmin'] = (getUserRole($username) === "1");
+ $_SESSION['folderOnly'] = loadUserPermissions($username);
+
+ unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'], $_SESSION['totp_failures']);
+
+ respond('ok', 200, 'Login successful');
+ }
+
+ // SETUP‑VERIFICATION flow
+ $username = $_SESSION['username'] ?? '';
+ if (!$username) {
+ respond('error', 400, 'Username not found in session');
+ }
+
+ /**
+ * Helper: retrieve the user's TOTP secret from users.txt
+ */
+ function getUserTOTPSecret(string $username): ?string {
+ global $encryptionKey;
+ $usersFile = USERS_DIR . USERS_FILE;
+ if (!file_exists($usersFile)) return null;
+ foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
+ $parts = explode(':', trim($line));
+ if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
+ return decryptData($parts[3], $encryptionKey);
+ }
+ }
+ return null;
+ }
+
+ $totpSecret = getUserTOTPSecret($username);
+ if (!$totpSecret) {
+ respond('error', 500, 'TOTP secret not found. Please set up TOTP again.');
+ }
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
+ if (!$tfa->verifyCode($totpSecret, $code)) {
+ $_SESSION['totp_failures']++;
+ respond('error', 400, 'Invalid TOTP code');
+ }
+
+ // success
+ unset($_SESSION['totp_failures']);
+ respond('ok', 200, 'TOTP successfully verified');
+
+} catch (\Throwable $e) {
+ // log error internally, then generic response
+ error_log("totp_verify error: " . $e->getMessage());
http_response_code(500);
- echo json_encode(["error" => "TOTP secret not found. Please try setting up TOTP again."]);
+ echo json_encode([
+ 'status' => 'error',
+ 'code' => 500,
+ 'message' => 'Internal server error'
+ ]);
exit;
-}
-
-// Verify the provided TOTP code.
-$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
-if (!$tfa->verifyCode($totpSecret, $totpCode)) {
- http_response_code(400);
- echo json_encode(["error" => "Invalid TOTP code."]);
- exit;
-}
-
-// If needed, you could update a flag or store the confirmation in the user record here.
-
-// Return a successful response.
-echo json_encode(["success" => true, "message" => "TOTP successfully verified."]);
-?>
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/uploads/.gitkeep b/uploads/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/uploads/.htaccess b/uploads/.htaccess
new file mode 100644
index 0000000..651f7db
--- /dev/null
+++ b/uploads/.htaccess
@@ -0,0 +1,7 @@
+
+ php_flag engine off
+
+
+ php_flag engine off
+
+ Options -Indexes
\ No newline at end of file
diff --git a/users/.gitkeep b/users/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/users/.htaccess b/users/.htaccess
new file mode 100644
index 0000000..b94f347
--- /dev/null
+++ b/users/.htaccess
@@ -0,0 +1,3 @@
+
+ Require all denied
+
\ No newline at end of file