extend TOTP to basic auth & OIDC. Fix share btn galleryview.
This commit is contained in:
43
.htaccess
43
.htaccess
@@ -4,26 +4,32 @@
|
|||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# 2) Default index files
|
# Default index files
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
DirectoryIndex index.html
|
DirectoryIndex index.html
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# 3) Deny access to hidden files
|
# Deny access to hidden files
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# (blocks access to .htaccess, .gitignore, etc.)
|
|
||||||
<FilesMatch "^\.">
|
<FilesMatch "^\.">
|
||||||
Require all denied
|
Require all denied
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# 4) Enforce HTTPS (optional)
|
# Enforce HTTPS (optional)
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Uncomment if you have SSL configured
|
RewriteEngine On
|
||||||
#RewriteEngine On
|
|
||||||
#RewriteCond %{HTTPS} off
|
#RewriteCond %{HTTPS} off
|
||||||
#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
#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>
|
<IfModule mod_headers.c>
|
||||||
# Prevent clickjacking
|
# Prevent clickjacking
|
||||||
Header always set X-Frame-Options "SAMEORIGIN"
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
@@ -40,9 +46,30 @@ DirectoryIndex index.html
|
|||||||
Header set Pragma "no-cache"
|
Header set Pragma "no-cache"
|
||||||
Header set Expires "0"
|
Header set Expires "0"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
# JS/CSS: short‑term cache, revalidate regularly
|
# JS/CSS: short‑term cache, revalidate regularly
|
||||||
<FilesMatch "\.(js|css)$">
|
<FilesMatch "\.(js|css)$">
|
||||||
Header set Cache-Control "public, max-age=3600, must-revalidate"
|
Header set Cache-Control "public, max-age=3600, must-revalidate"
|
||||||
</FilesMatch>
|
</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
111
auth.php
@@ -1,16 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|
||||||
|
// Only send the Content-Type header; CORS and related headers are handled via .htaccess.
|
||||||
header('Content-Type: application/json');
|
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.
|
* Helper: Get the user's role from users.txt.
|
||||||
*/
|
*/
|
||||||
function getUserRole($username) {
|
function getUserRole($username) {
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
foreach ($lines as $line) {
|
|
||||||
$parts = explode(":", trim($line));
|
$parts = explode(":", trim($line));
|
||||||
if (count($parts) >= 3 && $parts[0] === $username) {
|
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||||
return trim($parts[2]);
|
return trim($parts[2]);
|
||||||
@@ -21,37 +30,25 @@ function getUserRole($username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --- OIDC Authentication Flow --- */
|
/* --- OIDC Authentication Flow --- */
|
||||||
if (isset($_GET['oidc'])) {
|
// Detect either ?oidc=… or a callback that only has ?code=
|
||||||
// Read and decrypt OIDC configuration from JSON file.
|
$oidcAction = $_GET['oidc'] ?? null;
|
||||||
|
if (!$oidcAction && isset($_GET['code'])) {
|
||||||
|
$oidcAction = 'callback';
|
||||||
|
}
|
||||||
|
if ($oidcAction) {
|
||||||
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
$adminConfigFile = USERS_DIR . 'adminConfig.json';
|
||||||
if (file_exists($adminConfigFile)) {
|
if (file_exists($adminConfigFile)) {
|
||||||
$encryptedContent = file_get_contents($adminConfigFile);
|
$enc = file_get_contents($adminConfigFile);
|
||||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
$dec = decryptData($enc, $encryptionKey);
|
||||||
if ($decryptedContent === false) {
|
$cfg = $dec !== false ? json_decode($dec, true) : [];
|
||||||
// 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';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$oidc_provider_url = 'https://your-oidc-provider.com';
|
$cfg = [];
|
||||||
$oidc_client_id = 'YOUR_CLIENT_ID';
|
|
||||||
$oidc_client_secret = 'YOUR_CLIENT_SECRET';
|
|
||||||
$oidc_redirect_uri = 'https://yourdomain.com/auth.php?oidc=callback';
|
|
||||||
}
|
}
|
||||||
|
$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 = new Jumbojett\OpenIDConnectClient(
|
||||||
$oidc_provider_url,
|
$oidc_provider_url,
|
||||||
@@ -60,31 +57,54 @@ if (isset($_GET['oidc'])) {
|
|||||||
);
|
);
|
||||||
$oidc->setRedirectURL($oidc_redirect_uri);
|
$oidc->setRedirectURL($oidc_redirect_uri);
|
||||||
|
|
||||||
if ($_GET['oidc'] === 'callback') {
|
if ($oidcAction === 'callback') {
|
||||||
try {
|
try {
|
||||||
$oidc->authenticate();
|
$oidc->authenticate();
|
||||||
$username = $oidc->requestUserInfo('preferred_username');
|
$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_regenerate_id(true);
|
||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_SESSION["username"] = $username;
|
||||||
// Determine the user role from users.txt.
|
$_SESSION["isAdmin"] = (getUserRole($username) === "1");
|
||||||
$userRole = getUserRole($username);
|
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
||||||
$_SESSION["isAdmin"] = ($userRole === "1");
|
|
||||||
// *** Use loadUserPermissions() here instead of loadFolderPermission() ***
|
|
||||||
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
|
||||||
header("Location: index.html");
|
header("Location: index.html");
|
||||||
exit();
|
exit();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("OIDC authentication error: " . $e->getMessage());
|
error_log("OIDC authentication error: " . $e->getMessage());
|
||||||
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Authentication failed."]);
|
echo json_encode(["error" => "Authentication failed."]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Initiate OIDC authentication.
|
||||||
try {
|
try {
|
||||||
$oidc->authenticate();
|
$oidc->authenticate();
|
||||||
exit();
|
exit();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log("OIDC initiation error: " . $e->getMessage());
|
error_log("OIDC initiation error: " . $e->getMessage());
|
||||||
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Authentication initiation failed."]);
|
echo json_encode(["error" => "Authentication initiation failed."]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -92,10 +112,9 @@ if (isset($_GET['oidc'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --- Fallback: Form-based Authentication --- */
|
/* --- Fallback: Form-based Authentication --- */
|
||||||
// (Form-based branch code remains unchanged. It calls loadUserPermissions() in its basic auth branch.)
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
$maxAttempts = 5;
|
$maxAttempts = 5;
|
||||||
$lockoutTime = 30 * 60;
|
$lockoutTime = 30 * 60; // 30 minutes
|
||||||
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
||||||
$failedLogFile = USERS_DIR . 'failed_login.log';
|
$failedLogFile = USERS_DIR . 'failed_login.log';
|
||||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
@@ -111,7 +130,7 @@ function loadFailedAttempts($file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveFailedAttempts($file, $data) {
|
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'];
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
@@ -121,6 +140,7 @@ $failedAttempts = loadFailedAttempts($attemptsFile);
|
|||||||
if (isset($failedAttempts[$ip])) {
|
if (isset($failedAttempts[$ip])) {
|
||||||
$attemptData = $failedAttempts[$ip];
|
$attemptData = $failedAttempts[$ip];
|
||||||
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
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."]);
|
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -137,11 +157,9 @@ function authenticate($username, $password) {
|
|||||||
if (count($parts) < 3) continue;
|
if (count($parts) < 3) continue;
|
||||||
if ($username === $parts[0] && password_verify($password, $parts[1])) {
|
if ($username === $parts[0] && password_verify($password, $parts[1])) {
|
||||||
$result = ['role' => $parts[2]];
|
$result = ['role' => $parts[2]];
|
||||||
if (isset($parts[3]) && !empty($parts[3])) {
|
$result['totp_secret'] = (isset($parts[3]) && !empty($parts[3]))
|
||||||
$result['totp_secret'] = decryptData($parts[3], $encryptionKey);
|
? decryptData($parts[3], $encryptionKey)
|
||||||
} else {
|
: null;
|
||||||
$result['totp_secret'] = null;
|
|
||||||
}
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,11 +172,13 @@ $password = trim($data["password"] ?? "");
|
|||||||
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
||||||
|
|
||||||
if (!$username || !$password) {
|
if (!$username || !$password) {
|
||||||
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Username and password are required"]);
|
echo json_encode(["error" => "Username and password are required"]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
|
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."]);
|
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -167,6 +187,7 @@ $user = authenticate($username, $password);
|
|||||||
if ($user !== false) {
|
if ($user !== false) {
|
||||||
if (!empty($user['totp_secret'])) {
|
if (!empty($user['totp_secret'])) {
|
||||||
if (empty($data['totp_code'])) {
|
if (empty($data['totp_code'])) {
|
||||||
|
http_response_code(401);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"totp_required" => true,
|
"totp_required" => true,
|
||||||
"message" => "TOTP code required"
|
"message" => "TOTP code required"
|
||||||
@@ -176,6 +197,7 @@ if ($user !== false) {
|
|||||||
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
$tfa = new \RobThree\Auth\TwoFactorAuth('FileRise');
|
||||||
$providedCode = trim($data['totp_code']);
|
$providedCode = trim($data['totp_code']);
|
||||||
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
||||||
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Invalid TOTP code"]);
|
echo json_encode(["error" => "Invalid TOTP code"]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -229,6 +251,7 @@ if ($user !== false) {
|
|||||||
saveFailedAttempts($attemptsFile, $failedAttempts);
|
saveFailedAttempts($attemptsFile, $failedAttempts);
|
||||||
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
|
$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);
|
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
|
||||||
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Invalid credentials"]);
|
echo json_encode(["error" => "Invalid credentials"]);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
88
js/auth.js
88
js/auth.js
@@ -1,11 +1,16 @@
|
|||||||
import { sendRequest } from './networkUtils.js';
|
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 { loadFileList } from './fileListView.js';
|
||||||
import { initFileActions } from './fileActions.js';
|
import { initFileActions } from './fileActions.js';
|
||||||
import { renderFileTable } from './fileListView.js';
|
import { renderFileTable } from './fileListView.js';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { loadFolderTree } from './folderManager.js';
|
||||||
import {
|
import {
|
||||||
openTOTPLoginModal,
|
openTOTPLoginModal as originalOpenTOTPLoginModal,
|
||||||
openUserPanel,
|
openUserPanel,
|
||||||
openTOTPModal,
|
openTOTPModal,
|
||||||
closeTOTPModal,
|
closeTOTPModal,
|
||||||
@@ -24,6 +29,43 @@ const currentOIDCConfig = {
|
|||||||
};
|
};
|
||||||
window.currentOIDCConfig = 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 ----------------- */
|
/* ----------------- Utility Functions ----------------- */
|
||||||
function updateItemsPerPageSelect() {
|
function updateItemsPerPageSelect() {
|
||||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||||
@@ -85,7 +127,6 @@ function updateAuthenticatedUI(data) {
|
|||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.username) {
|
if (data.username) {
|
||||||
localStorage.setItem("username", data.username);
|
localStorage.setItem("username", data.username);
|
||||||
}
|
}
|
||||||
@@ -103,11 +144,8 @@ function updateAuthenticatedUI(data) {
|
|||||||
restoreBtn.id = "restoreFilesBtn";
|
restoreBtn.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
restoreBtn.classList.add("btn", "btn-warning");
|
||||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||||
if (firstButton) {
|
if (firstButton) insertAfter(restoreBtn, firstButton);
|
||||||
insertAfter(restoreBtn, firstButton);
|
else headerButtons.appendChild(restoreBtn);
|
||||||
} else {
|
|
||||||
headerButtons.appendChild(restoreBtn);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
restoreBtn.style.display = "block";
|
restoreBtn.style.display = "block";
|
||||||
|
|
||||||
@@ -128,7 +166,7 @@ function updateAuthenticatedUI(data) {
|
|||||||
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
const adminPanelBtn = document.getElementById("adminPanelBtn");
|
||||||
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
if (adminPanelBtn) adminPanelBtn.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.location.hostname !== "demo.filerise.net") {
|
if (window.location.hostname !== "demo.filerise.net") {
|
||||||
let userPanelBtn = document.getElementById("userPanelBtn");
|
let userPanelBtn = document.getElementById("userPanelBtn");
|
||||||
if (!userPanelBtn) {
|
if (!userPanelBtn) {
|
||||||
@@ -136,17 +174,10 @@ function updateAuthenticatedUI(data) {
|
|||||||
userPanelBtn.id = "userPanelBtn";
|
userPanelBtn.id = "userPanelBtn";
|
||||||
userPanelBtn.classList.add("btn", "btn-user");
|
userPanelBtn.classList.add("btn", "btn-user");
|
||||||
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
|
userPanelBtn.innerHTML = '<i class="material-icons" title="User Panel">account_circle</i>';
|
||||||
let adminPanelBtn = document.getElementById("adminPanelBtn");
|
const adminBtn = document.getElementById("adminPanelBtn");
|
||||||
if (adminPanelBtn) {
|
if (adminBtn) insertAfter(userPanelBtn, adminBtn);
|
||||||
insertAfter(userPanelBtn, adminPanelBtn);
|
else if (firstButton) insertAfter(userPanelBtn, firstButton);
|
||||||
} else {
|
else headerButtons.appendChild(userPanelBtn);
|
||||||
const firstButton = headerButtons.firstElementChild;
|
|
||||||
if (firstButton) {
|
|
||||||
insertAfter(userPanelBtn, firstButton);
|
|
||||||
} else {
|
|
||||||
headerButtons.appendChild(userPanelBtn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userPanelBtn.addEventListener("click", openUserPanel);
|
userPanelBtn.addEventListener("click", openUserPanel);
|
||||||
} else {
|
} else {
|
||||||
userPanelBtn.style.display = "block";
|
userPanelBtn.style.display = "block";
|
||||||
@@ -193,6 +224,7 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
/* ----------------- Authentication Submission ----------------- */
|
/* ----------------- Authentication Submission ----------------- */
|
||||||
function submitLogin(data) {
|
function submitLogin(data) {
|
||||||
setLastLoginData(data);
|
setLastLoginData(data);
|
||||||
|
window.__lastLoginData = data;
|
||||||
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
sendRequest("auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -220,7 +252,7 @@ function submitLogin(data) {
|
|||||||
}
|
}
|
||||||
window.submitLogin = submitLogin;
|
window.submitLogin = submitLogin;
|
||||||
|
|
||||||
/* ----------------- Other Helpers and Initialization ----------------- */
|
/* ----------------- Other Helpers ----------------- */
|
||||||
window.changeItemsPerPage = function (value) {
|
window.changeItemsPerPage = function (value) {
|
||||||
localStorage.setItem("itemsPerPage", value);
|
localStorage.setItem("itemsPerPage", value);
|
||||||
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
if (typeof renderFileTable === "function") renderFileTable(window.currentFolder || "root");
|
||||||
@@ -259,7 +291,7 @@ function loadUserList() {
|
|||||||
closeRemoveUserModal();
|
closeRemoveUserModal();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
window.loadUserList = loadUserList;
|
window.loadUserList = loadUserList;
|
||||||
|
|
||||||
@@ -286,7 +318,7 @@ function initAuth() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
headers: { "X-CSRF-Token": window.csrfToken }
|
||||||
}).then(() => window.location.reload(true)).catch(() => { });
|
}).then(() => window.location.reload(true)).catch(() => {});
|
||||||
});
|
});
|
||||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
@@ -352,7 +384,7 @@ function initAuth() {
|
|||||||
showToast("Error: " + (data.error || "Could not remove user"));
|
showToast("Error: " + (data.error || "Could not remove user"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
document.getElementById("cancelRemoveUserBtn").addEventListener("click", closeRemoveUserModal);
|
||||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||||
@@ -404,13 +436,19 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||||
});
|
});
|
||||||
|
|
||||||
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
if (oidcLoginBtn) {
|
if (oidcLoginBtn) {
|
||||||
oidcLoginBtn.addEventListener("click", () => {
|
oidcLoginBtn.addEventListener("click", () => {
|
||||||
// Redirect to the OIDC auth endpoint. The endpoint can be adjusted if needed.
|
|
||||||
window.location.href = "auth.php?oidc=initiate";
|
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 };
|
export { initAuth, checkAuthentication };
|
||||||
123
js/authModals.js
123
js/authModals.js
@@ -3,61 +3,102 @@ import { sendRequest } from './networkUtils.js';
|
|||||||
|
|
||||||
const version = "v1.0.7";
|
const version = "v1.0.7";
|
||||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||||
let lastLoginData = null;
|
|
||||||
|
|
||||||
|
let lastLoginData = null;
|
||||||
export function setLastLoginData(data) {
|
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() {
|
export function openTOTPLoginModal() {
|
||||||
let totpLoginModal = document.getElementById("totpLoginModal");
|
let totpLoginModal = document.getElementById("totpLoginModal");
|
||||||
const isDarkMode = document.body.classList.contains("dark-mode");
|
const isDarkMode = document.body.classList.contains("dark-mode");
|
||||||
const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
|
const modalBg = isDarkMode ? "#2c2c2c" : "#fff";
|
||||||
const textColor = isDarkMode ? "#e0e0e0" : "#000";
|
const textColor = isDarkMode ? "#e0e0e0" : "#000";
|
||||||
|
|
||||||
if (!totpLoginModal) {
|
if (!totpLoginModal) {
|
||||||
totpLoginModal = document.createElement("div");
|
totpLoginModal = document.createElement("div");
|
||||||
totpLoginModal.id = "totpLoginModal";
|
totpLoginModal.id = "totpLoginModal";
|
||||||
totpLoginModal.style.cssText = `
|
totpLoginModal.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0; left: 0;
|
||||||
left: 0;
|
width: 100vw; height: 100vh;
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(0,0,0,0.5);
|
background-color: rgba(0,0,0,0.5);
|
||||||
display: flex;
|
display: flex; justify-content: center; align-items: center;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 3200;
|
z-index: 3200;
|
||||||
`;
|
`;
|
||||||
totpLoginModal.innerHTML = `
|
totpLoginModal.innerHTML = `
|
||||||
<div style="background: ${modalBg}; padding: 20px; border-radius: 8px; text-align: center; position: relative; color: ${textColor};">
|
<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;">×</span>
|
<span id="closeTOTPLoginModal" style="position:absolute; top:10px; right:10px; cursor:pointer; font-size:24px;">×</span>
|
||||||
<h3>Enter TOTP Code</h3>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(totpLoginModal);
|
document.body.appendChild(totpLoginModal);
|
||||||
document.getElementById("closeTOTPLoginModal").addEventListener("click", () => {
|
|
||||||
totpLoginModal.style.display = "none";
|
document.getElementById("closeTOTPLoginModal").addEventListener("click", () => {
|
||||||
});
|
totpLoginModal.style.display = "none";
|
||||||
const totpInput = document.getElementById("totpLoginInput");
|
});
|
||||||
totpInput.focus();
|
|
||||||
totpInput.addEventListener("input", function () {
|
const totpInput = document.getElementById("totpLoginInput");
|
||||||
if (this.value.trim().length === 6 && lastLoginData) {
|
totpInput.focus();
|
||||||
lastLoginData.totp_code = this.value.trim();
|
|
||||||
totpLoginModal.style.display = "none";
|
totpInput.addEventListener("input", function () {
|
||||||
if (typeof window.submitLogin === "function") {
|
const code = this.value.trim();
|
||||||
window.submitLogin(lastLoginData);
|
if (code.length === 6) {
|
||||||
}
|
// FORM-BASED LOGIN
|
||||||
}
|
if (lastLoginData) {
|
||||||
});
|
totpLoginModal.style.display = "none";
|
||||||
} else {
|
lastLoginData.totp_code = code;
|
||||||
totpLoginModal.style.display = "flex";
|
window.submitLogin(lastLoginData);
|
||||||
const modalContent = totpLoginModal.firstElementChild;
|
|
||||||
modalContent.style.background = modalBg;
|
// BASIC-AUTH / OIDC LOGIN
|
||||||
modalContent.style.color = textColor;
|
} 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() {
|
export function openUserPanel() {
|
||||||
|
|||||||
@@ -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">
|
<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>
|
<i class="material-icons">drive_file_rename_outline</i>
|
||||||
</button>
|
</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>
|
<i class="material-icons">share</i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,10 +306,23 @@ export function renderGalleryView(folder) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
galleryHTML += "</div>";
|
galleryHTML += "</div>";
|
||||||
|
|
||||||
fileListContainer.innerHTML = galleryHTML;
|
fileListContainer.innerHTML = galleryHTML;
|
||||||
|
|
||||||
createViewToggleButton();
|
createViewToggleButton();
|
||||||
updateFileActionButtons();
|
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) {
|
export function sortFiles(column, folder) {
|
||||||
|
|||||||
@@ -253,4 +253,5 @@ export function displayFilePreview(file, container) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.previewFile = previewFile;
|
window.previewFile = previewFile;
|
||||||
|
window.openShareModal = openShareModal;
|
||||||
100
login_basic.php
100
login_basic.php
@@ -3,6 +3,19 @@ require_once 'config.php';
|
|||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE; // Make sure the users file path is defined
|
$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
|
// Reuse the same authentication function
|
||||||
function authenticate($username, $password)
|
function authenticate($username, $password)
|
||||||
{
|
{
|
||||||
@@ -43,15 +56,9 @@ function loadFolderPermission($username) {
|
|||||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||||
if (file_exists($permissionsFile)) {
|
if (file_exists($permissionsFile)) {
|
||||||
$content = file_get_contents($permissionsFile);
|
$content = file_get_contents($permissionsFile);
|
||||||
// Try to decrypt the content.
|
$decrypted = decryptData($content, $encryptionKey);
|
||||||
$decryptedContent = decryptData($content, $encryptionKey);
|
$permissions = $decrypted !== false ? json_decode($decrypted, true) : json_decode($content, true);
|
||||||
if ($decryptedContent !== false) {
|
|
||||||
$permissions = json_decode($decryptedContent, true);
|
|
||||||
} else {
|
|
||||||
$permissions = json_decode($content, true);
|
|
||||||
}
|
|
||||||
if (is_array($permissions)) {
|
if (is_array($permissions)) {
|
||||||
// Use case-insensitive comparison.
|
|
||||||
foreach ($permissions as $storedUsername => $data) {
|
foreach ($permissions as $storedUsername => $data) {
|
||||||
if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) {
|
if (strcasecmp($storedUsername, $username) === 0 && isset($data['folderOnly'])) {
|
||||||
return (bool)$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.
|
// 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');
|
header('HTTP/1.0 401 Unauthorized');
|
||||||
echo 'Authorization Required';
|
echo 'Authorization Required';
|
||||||
exit;
|
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;
|
||||||
?>
|
?>
|
||||||
203
totp_verify.php
203
totp_verify.php
@@ -1,84 +1,153 @@
|
|||||||
<?php
|
<?php
|
||||||
// verifyTOTPSetup.php
|
// totp_verify.php
|
||||||
|
|
||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
// Secure session cookie
|
||||||
http_response_code(403);
|
session_set_cookie_params([
|
||||||
echo json_encode(["error" => "Not authenticated"]);
|
'lifetime' => 0,
|
||||||
exit;
|
'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.
|
// JSON + CSP
|
||||||
$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.
|
|
||||||
header('Content-Type: application/json');
|
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.
|
try {
|
||||||
$input = json_decode(file_get_contents("php://input"), true);
|
// standardized error helper
|
||||||
if (!isset($input['totp_code']) || strlen(trim($input['totp_code'])) !== 6 || !ctype_digit(trim($input['totp_code']))) {
|
function respond($status, $code, $message, $data = []) {
|
||||||
http_response_code(400);
|
http_response_code($code);
|
||||||
echo json_encode(["error" => "A valid 6-digit TOTP code is required"]);
|
echo json_encode([
|
||||||
exit;
|
'status' => $status,
|
||||||
}
|
'code' => $code,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => $data
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$totpCode = trim($input['totp_code']);
|
// Rate‑limit TOTP attempts
|
||||||
$username = $_SESSION['username'] ?? '';
|
if (!isset($_SESSION['totp_failures'])) {
|
||||||
if (empty($username)) {
|
$_SESSION['totp_failures'] = 0;
|
||||||
http_response_code(400);
|
}
|
||||||
echo json_encode(["error" => "Username not found in session"]);
|
if ($_SESSION['totp_failures'] >= 5) {
|
||||||
exit;
|
respond('error', 429, 'Too many TOTP attempts. Please try again later.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the current user's TOTP secret from users.txt.
|
* Helper: Get a user's role from users.txt
|
||||||
*
|
*/
|
||||||
* @param string $username
|
function getUserRole(string $username): ?string {
|
||||||
* @return string|null The decrypted TOTP secret or null if not found.
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
*/
|
if (!file_exists($usersFile)) return null;
|
||||||
function getUserTOTPSecret($username) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
global $encryptionKey;
|
$parts = explode(':', trim($line));
|
||||||
// Define the path to your users file.
|
if (count($parts) >= 3 && $parts[0] === $username) {
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
return trim($parts[2]);
|
||||||
if (!file_exists($usersFile)) {
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$lines = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
||||||
foreach ($lines as $line) {
|
// Must be authenticated or pending TOTP
|
||||||
$parts = explode(':', trim($line));
|
if (
|
||||||
// Assuming format: username:hashedPassword:role:encryptedTOTPSecret
|
!(
|
||||||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
(isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true)
|
||||||
return decryptData($parts[3], $encryptionKey);
|
|| isset($_SESSION['pending_login_user'])
|
||||||
}
|
)
|
||||||
|
) {
|
||||||
|
respond('error', 403, 'Not authenticated');
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the user's TOTP secret.
|
// CSRF check
|
||||||
$totpSecret = getUserTOTPSecret($username);
|
$csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
if (!$totpSecret) {
|
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);
|
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;
|
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
0
uploads/.gitkeep
Normal file
7
uploads/.htaccess
Normal file
7
uploads/.htaccess
Normal 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
0
users/.gitkeep
Normal file
3
users/.htaccess
Normal file
3
users/.htaccess
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Files "users.txt">
|
||||||
|
Require all denied
|
||||||
|
</Files>
|
||||||
Reference in New Issue
Block a user