extend TOTP to basic auth & OIDC. Fix share btn galleryview.

This commit is contained in:
Ryan
2025-04-05 22:22:47 -04:00
committed by GitHub
parent 899b04e49a
commit 5100e8bf3b
12 changed files with 466 additions and 230 deletions

View File

@@ -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.)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# -----------------------------
# 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]
<IfModule mod_headers.c>
# 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"
</IfModule>
<IfModule mod_headers.c>
# 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"
</FilesMatch>
# JS/CSS: shortterm cache, revalidate regularly
<FilesMatch "\.(js|css)$">
Header set Cache-Control "public, max-age=3600, must-revalidate"
</FilesMatch>
</IfModule>
</IfModule>
# -----------------------------
# Additional Security Headers
# -----------------------------
<IfModule mod_headers.c>
# 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"
</IfModule>
# -----------------------------
# Disable TRACE method
# -----------------------------
RewriteCond %{REQUEST_METHOD} ^TRACE
RewriteRule .* - [F]

111
auth.php
View File

@@ -1,16 +1,25 @@
<?php
require_once 'vendor/autoload.php';
require_once 'config.php';
// Only send the Content-Type header; CORS and related headers are handled via .htaccess.
header('Content-Type: application/json');
// Global exception handler: logs errors and returns a generic error message.
set_exception_handler(function ($e) {
error_log("Unhandled exception: " . $e->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"]);
}
?>

View File

@@ -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 were in a pendingTOTP 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 BasicAuth 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 = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
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 = '<i class="material-icons" title="User Panel">account_circle</i>';
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 };

View File

@@ -3,61 +3,102 @@ import { sendRequest } from './networkUtils.js';
const version = "v1.0.7";
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
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 = `
<div style="background: ${modalBg}; padding: 20px; border-radius: 8px; text-align: center; position: relative; color: ${textColor};">
<span id="closeTOTPLoginModal" style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 24px;">&times;</span>
totpLoginModal.innerHTML = `
<div style="background: ${modalBg}; padding:20px; border-radius:8px; text-align:center; position:relative; color:${textColor};">
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">&times;</span>
<h3>Enter TOTP Code</h3>
<input type="text" id="totpLoginInput" maxlength="6" style="font-size:24px; text-align:center; width:100%; padding:10px;" placeholder="6-digit code" />
<input type="text" id="totpLoginInput" maxlength="6"
style="font-size:24px; text-align:center; width:100%; padding:10px;"
placeholder="6-digit code" />
</div>
`;
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() {

View File

@@ -297,7 +297,7 @@ export function renderGalleryView(folder) {
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Rename">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<button class="btn btn-sm btn-secondary share-btn" onclick='openShareModal(${JSON.stringify(file)}, ${JSON.stringify(folder)})' title="Share">
<button class="btn btn-sm btn-secondary share-btn" data-file="${escapeHTML(file.name)}" title="Share">
<i class="material-icons">share</i>
</button>
</div>
@@ -306,10 +306,23 @@ export function renderGalleryView(folder) {
});
galleryHTML += "</div>";
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) {

View File

@@ -253,4 +253,5 @@ export function displayFilePreview(file, container) {
}
}
window.previewFile = previewFile;
window.previewFile = previewFile;
window.openShareModal = openShareModal;

View File

@@ -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;
?>

View File

@@ -1,84 +1,153 @@
<?php
// verifyTOTPSetup.php
// totp_verify.php
require_once 'vendor/autoload.php';
require_once 'config.php';
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(403);
echo json_encode(["error" => "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;
}
// Ratelimit 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 (BasicAuth 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');
}
// SETUPVERIFICATION 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."]);
?>
}

0
uploads/.gitkeep Normal file
View File

7
uploads/.htaccess Normal file
View File

@@ -0,0 +1,7 @@
<IfModule mod_php7.c>
php_flag engine off
</IfModule>
<IfModule mod_php.c>
php_flag engine off
</IfModule>
Options -Indexes

0
users/.gitkeep Normal file
View File

3
users/.htaccess Normal file
View File

@@ -0,0 +1,3 @@
<Files "users.txt">
Require all denied
</Files>