Overhaul networkUtils and expand auth
This commit is contained in:
@@ -26,6 +26,13 @@ This refactor improves maintainability, testability, and documentation clarity a
|
|||||||
- Switched your share modal code to use a leading slash ("/api/file/share.php") so it generates absolute URLs instead of relative /share.php.
|
- Switched your share modal code to use a leading slash ("/api/file/share.php") so it generates absolute URLs instead of relative /share.php.
|
||||||
- In the shared‑folder gallery, adjusted the client‑side image path to point at /uploads/... instead of /api/folder/uploads/...
|
- In the shared‑folder gallery, adjusted the client‑side image path to point at /uploads/... instead of /api/folder/uploads/...
|
||||||
- Updated both AdminModel defaults and the AuthController to use the exact full path
|
- Updated both AdminModel defaults and the AuthController to use the exact full path
|
||||||
|
- Network Utilities Overhaul swapped out the old fetch wrapper for one that always reads the raw response, tries to JSON.parse it, and then either returns the parsed object on ok or throws it on error.
|
||||||
|
- Adjusted your submitLogin .catch() to grab the thrown object (or string) and pass that through to showToast, so now “Invalid credentials” actually shows up.
|
||||||
|
- Pulled the common session‑setup and “remember me” logic into two new helpers, finalizeLogin() (for AJAX/form/basic/TOTP) and finishBrowserLogin() (for OIDC redirects). That removed tons of duplication and ensures every path calls the same permission‑loading code.
|
||||||
|
- Ensured that after you POST just a totp_code, we pick up pending_login_user/pending_login_secret, verify it, then immediately call finalizeLogin().
|
||||||
|
- Expanded checkAuth.php Response now returns all three flags—folderOnly, readOnly, and disableUpload so client can handle every permission.
|
||||||
|
- In auth.js’s updateAuthenticatedUI(), write all three flags into localStorage whenever you land on the app (OIDC, basic or form). That guarantees consistent behavior across page loads.
|
||||||
|
- Made sure the OIDC handler reads the live config via AdminModel::getConfig() and pushes you through the TOTP flow if needed, then back to /index.html.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -118,10 +118,10 @@ $cookieParams = [
|
|||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
];
|
];
|
||||||
// At the very beginning of config.php
|
// At the very beginning of config.php
|
||||||
ini_set('session.save_path', __DIR__ . '/../sessions');
|
/*ini_set('session.save_path', __DIR__ . '/../sessions');
|
||||||
if (!is_dir(__DIR__ . '/../sessions')) {
|
if (!is_dir(__DIR__ . '/../sessions')) {
|
||||||
mkdir(__DIR__ . '/../sessions', 0777, true);
|
mkdir(__DIR__ . '/../sessions', 0777, true);
|
||||||
}
|
}*/
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_set_cookie_params($cookieParams);
|
session_set_cookie_params($cookieParams);
|
||||||
ini_set('session.gc_maxlifetime', 7200);
|
ini_set('session.gc_maxlifetime', 7200);
|
||||||
@@ -164,7 +164,7 @@ define('BASE_URL', 'http://yourwebsite/uploads/');
|
|||||||
|
|
||||||
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||||
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
$defaultShareUrl = isset($_SERVER['HTTP_HOST'])
|
||||||
? "http://" . $_SERVER['HTTP_HOST'] . "/share.php"
|
? "http://" . $_SERVER['HTTP_HOST'] . "/api/file/share.php"
|
||||||
: "http://localhost/api/file/share.php";
|
: "http://localhost/api/file/share.php";
|
||||||
} else {
|
} else {
|
||||||
$defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php";
|
$defaultShareUrl = rtrim(BASE_URL, '/') . "/api/file/share.php";
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||||
<meta name="csrf-token" content="">
|
<meta name="csrf-token" content="">
|
||||||
<meta name="share-url" content="/api/file/share.php">
|
<meta name="share-url" content="">
|
||||||
<!-- Google Fonts and Material Icons -->
|
<!-- Google Fonts and Material Icons -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
|
|||||||
@@ -149,11 +149,12 @@ function updateAuthenticatedUI(data) {
|
|||||||
if (data.username) {
|
if (data.username) {
|
||||||
localStorage.setItem("username", data.username);
|
localStorage.setItem("username", data.username);
|
||||||
}
|
}
|
||||||
/*
|
if (typeof data.folderOnly !== "undefined") {
|
||||||
if (typeof data.folderOnly !== "undefined") {
|
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
||||||
localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false");
|
localStorage.setItem("readOnly", data.readOnly ? "true" : "false");
|
||||||
|
localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false");
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
const headerButtons = document.querySelector(".header-buttons");
|
||||||
const firstButton = headerButtons.firstElementChild;
|
const firstButton = headerButtons.firstElementChild;
|
||||||
|
|
||||||
@@ -227,6 +228,10 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
}
|
}
|
||||||
window.setupMode = false;
|
window.setupMode = false;
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
|
localStorage.setItem("folderOnly", data.folderOnly );
|
||||||
|
localStorage.setItem("readOnly", data.readOnly );
|
||||||
|
localStorage.setItem("disableUpload",data.disableUpload);
|
||||||
|
updateLoginOptionsUIFromStorage();
|
||||||
if (typeof data.totp_enabled !== "undefined") {
|
if (typeof data.totp_enabled !== "undefined") {
|
||||||
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false");
|
||||||
}
|
}
|
||||||
@@ -249,6 +254,7 @@ function checkAuthentication(showLoginToast = true) {
|
|||||||
function submitLogin(data) {
|
function submitLogin(data) {
|
||||||
setLastLoginData(data);
|
setLastLoginData(data);
|
||||||
window.__lastLoginData = data;
|
window.__lastLoginData = data;
|
||||||
|
|
||||||
sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
sendRequest("api/auth/auth.php", "POST", data, { "X-CSRF-Token": window.csrfToken })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.success || response.status === "ok") {
|
if (response.success || response.status === "ok") {
|
||||||
@@ -263,7 +269,7 @@ function submitLogin(data) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// if fetching permissions fails.
|
// ignore permission‐fetch errors
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -272,7 +278,7 @@ function submitLogin(data) {
|
|||||||
openTOTPLoginModal();
|
openTOTPLoginModal();
|
||||||
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
} else if (response.error && response.error.includes("Too many failed login attempts")) {
|
||||||
showToast(response.error);
|
showToast(response.error);
|
||||||
const loginButton = document.getElementById("authForm").querySelector("button[type='submit']");
|
const loginButton = document.querySelector("#authForm button[type='submit']");
|
||||||
if (loginButton) {
|
if (loginButton) {
|
||||||
loginButton.disabled = true;
|
loginButton.disabled = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -284,10 +290,18 @@ function submitLogin(data) {
|
|||||||
showToast("Login failed: " + (response.error || "Unknown error"));
|
showToast("Login failed: " + (response.error || "Unknown error"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(err => {
|
||||||
showToast("Login failed: Unknown error");
|
// err may be an Error object or a string
|
||||||
|
let msg = "Unknown error";
|
||||||
|
if (err && typeof err === "object") {
|
||||||
|
msg = err.error || err.message || msg;
|
||||||
|
} else if (typeof err === "string") {
|
||||||
|
msg = err;
|
||||||
|
}
|
||||||
|
showToast(`Login failed: ${msg}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.submitLogin = submitLogin;
|
window.submitLogin = submitLogin;
|
||||||
|
|
||||||
/* ----------------- Other Helpers ----------------- */
|
/* ----------------- Other Helpers ----------------- */
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
|
// public/js/networkUtils.js
|
||||||
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
|
export function sendRequest(url, method = "GET", data = null, customHeaders = {}) {
|
||||||
const options = {
|
const options = {
|
||||||
method,
|
method,
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {}
|
headers: { ...customHeaders }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Merge custom headers
|
|
||||||
Object.assign(options.headers, customHeaders);
|
|
||||||
|
|
||||||
// If data is provided and is not FormData, assume JSON.
|
|
||||||
if (data && !(data instanceof FormData)) {
|
if (data && !(data instanceof FormData)) {
|
||||||
if (!options.headers["Content-Type"]) {
|
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
|
||||||
options.headers["Content-Type"] = "application/json";
|
|
||||||
}
|
|
||||||
options.body = JSON.stringify(data);
|
options.body = JSON.stringify(data);
|
||||||
} else if (data instanceof FormData) {
|
} else if (data instanceof FormData) {
|
||||||
options.body = data;
|
options.body = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url, options)
|
return fetch(url, options)
|
||||||
.then(response => {
|
.then(async res => {
|
||||||
if (!response.ok) {
|
const text = await res.text();
|
||||||
return response.text().then(text => {
|
let payload;
|
||||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
try {
|
||||||
});
|
payload = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
payload = text;
|
||||||
}
|
}
|
||||||
const clonedResponse = response.clone();
|
if (!res.ok) {
|
||||||
return response.json().catch(() => clonedResponse.text());
|
// Reject with the parsed JSON (or raw text) so .catch(error) gets it
|
||||||
|
throw payload;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,209 +58,233 @@ class AuthController
|
|||||||
*
|
*
|
||||||
* @return void Redirects on success or outputs JSON error.
|
* @return void Redirects on success or outputs JSON error.
|
||||||
*/
|
*/
|
||||||
|
// in src/controllers/AuthController.php
|
||||||
|
|
||||||
public function auth(): void
|
public function auth(): void
|
||||||
{
|
{
|
||||||
// Global exception handler.
|
header('Content-Type: application/json');
|
||||||
set_exception_handler(function ($e) {
|
set_exception_handler(function ($e) {
|
||||||
error_log("Unhandled exception: " . $e->getMessage());
|
error_log("Unhandled exception: " . $e->getMessage());
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(["error" => "Internal Server Error"]);
|
echo json_encode(['error' => 'Internal Server Error']);
|
||||||
exit();
|
exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
// Decode any JSON payload
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||||
|
$username = trim($data['username'] ?? '');
|
||||||
|
$password = trim($data['password'] ?? '');
|
||||||
|
$totpCode = trim($data['totp_code'] ?? '');
|
||||||
|
$rememberMe = !empty($data['remember_me']);
|
||||||
|
|
||||||
// If OIDC parameters are present, initiate OIDC flow.
|
//
|
||||||
|
// 1) TOTP‑only step: user already passed credentials and we asked for TOTP,
|
||||||
|
// now they POST just totp_code.
|
||||||
|
//
|
||||||
|
if ($totpCode && isset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret'])) {
|
||||||
|
$username = $_SESSION['pending_login_user'];
|
||||||
|
$secret = $_SESSION['pending_login_secret'];
|
||||||
|
|
||||||
|
$tfa = new TwoFactorAuth(new GoogleChartsQrCodeProvider(), 'FileRise', 6, 30, Algorithm::Sha1);
|
||||||
|
if (! $tfa->verifyCode($secret, $totpCode)) {
|
||||||
|
echo json_encode(['error' => 'Invalid TOTP code']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
// clear the pending markers
|
||||||
|
unset($_SESSION['pending_login_user'], $_SESSION['pending_login_secret']);
|
||||||
|
// now finish login
|
||||||
|
$this->finalizeLogin($username, $rememberMe);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2) OIDC flow
|
||||||
|
//
|
||||||
$oidcAction = $_GET['oidc'] ?? null;
|
$oidcAction = $_GET['oidc'] ?? null;
|
||||||
if (!$oidcAction && isset($_GET['code'])) {
|
if (! $oidcAction && isset($_GET['code'])) {
|
||||||
$oidcAction = 'callback';
|
$oidcAction = 'callback';
|
||||||
}
|
}
|
||||||
if ($oidcAction) {
|
if ($oidcAction) {
|
||||||
// new: delegate to AdminModel
|
|
||||||
$cfg = AdminModel::getConfig();
|
$cfg = AdminModel::getConfig();
|
||||||
// Optional: log to confirm you loaded the right values
|
$oidc = new OpenIDConnectClient(
|
||||||
error_log("Loaded OIDC config: " . print_r($cfg['oidc'], true));
|
$cfg['oidc']['providerUrl'],
|
||||||
|
$cfg['oidc']['clientId'],
|
||||||
$oidc_provider_url = $cfg['oidc']['providerUrl'];
|
$cfg['oidc']['clientSecret']
|
||||||
$oidc_client_id = $cfg['oidc']['clientId'];
|
);
|
||||||
$oidc_client_secret = $cfg['oidc']['clientSecret'];
|
$oidc->setRedirectURL($cfg['oidc']['redirectUri']);
|
||||||
$oidc_redirect_uri = $cfg['oidc']['redirectUri'];
|
|
||||||
$oidc = new OpenIDConnectClient($oidc_provider_url, $oidc_client_id, $oidc_client_secret);
|
|
||||||
$oidc->setRedirectURL($oidc_redirect_uri);
|
|
||||||
|
|
||||||
if ($oidcAction === 'callback') {
|
if ($oidcAction === 'callback') {
|
||||||
try {
|
try {
|
||||||
$oidc->authenticate();
|
$oidc->authenticate();
|
||||||
$username = $oidc->requestUserInfo('preferred_username');
|
$username = $oidc->requestUserInfo('preferred_username');
|
||||||
|
|
||||||
// Check for TOTP secret.
|
// check if this user has a TOTP secret
|
||||||
$totp_secret = null;
|
$totp_secret = null;
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
if (file_exists($usersFile)) {
|
if (file_exists($usersFile)) {
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
$parts = explode(":", trim($line));
|
$parts = explode(':', trim($line));
|
||||||
if (count($parts) >= 4 && $parts[0] === $username && !empty($parts[3])) {
|
if (count($parts) >= 4 && $parts[0] === $username && $parts[3] !== '') {
|
||||||
$totp_secret = decryptData($parts[3], $encryptionKey);
|
$totp_secret = decryptData($parts[3], $GLOBALS['encryptionKey']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($totp_secret) {
|
if ($totp_secret) {
|
||||||
$_SESSION['pending_login_user'] = $username;
|
$_SESSION['pending_login_user'] = $username;
|
||||||
$_SESSION['pending_login_secret'] = $totp_secret;
|
$_SESSION['pending_login_secret'] = $totp_secret;
|
||||||
header("Location: /index.html?totp_required=1");
|
header('Location: /index.html?totp_required=1');
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize login (no TOTP)
|
// no TOTP → finish immediately
|
||||||
session_regenerate_id(true);
|
$this->finishBrowserLogin($username);
|
||||||
$_SESSION["authenticated"] = true;
|
} catch (\Exception $e) {
|
||||||
$_SESSION["username"] = $username;
|
error_log("OIDC auth error: " . $e->getMessage());
|
||||||
$_SESSION["isAdmin"] = (AuthModel::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);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Authentication failed."]);
|
echo json_encode(['error' => 'Authentication failed.']);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Initiate OIDC authentication.
|
// initial OIDC redirect
|
||||||
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);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Authentication initiation failed."]);
|
echo json_encode(['error' => 'Authentication initiation failed.']);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Form-based Authentication.
|
//
|
||||||
$data = json_decode(file_get_contents("php://input"), true);
|
// 3) Form‑based / AJAX login
|
||||||
$username = trim($data["username"] ?? "");
|
//
|
||||||
$password = trim($data["password"] ?? "");
|
if (! $username || ! $password) {
|
||||||
$rememberMe = isset($data["remember_me"]) && $data["remember_me"] === true;
|
|
||||||
|
|
||||||
if (!$username || !$password) {
|
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(["error" => "Username and password are required"]);
|
echo json_encode(['error' => 'Username and password are required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
if (! preg_match(REGEX_USER, $username)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid username format']);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preg_match(REGEX_USER, $username)) {
|
// rate‑limit
|
||||||
http_response_code(400);
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
echo json_encode(["error" => "Invalid username format. Only letters, numbers, underscores, dashes, and spaces are allowed."]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$ip = $_SERVER['REMOTE_ADDR'];
|
|
||||||
$currentTime = time();
|
|
||||||
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
$attemptsFile = USERS_DIR . 'failed_logins.json';
|
||||||
$failedAttempts = AuthModel::loadFailedAttempts($attemptsFile);
|
$failed = AuthModel::loadFailedAttempts($attemptsFile);
|
||||||
$maxAttempts = 5;
|
if (
|
||||||
$lockoutTime = 30 * 60; // 30 minutes
|
isset($failed[$ip]) &&
|
||||||
|
$failed[$ip]['count'] >= 5 &&
|
||||||
if (isset($failedAttempts[$ip])) {
|
time() - $failed[$ip]['last_attempt'] < 30 * 60
|
||||||
$attemptData = $failedAttempts[$ip];
|
) {
|
||||||
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
|
http_response_code(429);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = AuthModel::authenticate($username, $password);
|
$user = AuthModel::authenticate($username, $password);
|
||||||
if ($user !== false) {
|
if ($user === false) {
|
||||||
// Handle TOTP if required.
|
// record failure
|
||||||
if (!empty($user['totp_secret'])) {
|
$failed[$ip] = [
|
||||||
if (empty($data['totp_code']) || !preg_match('/^\d{6}$/', $data['totp_code'])) {
|
'count' => ($failed[$ip]['count'] ?? 0) + 1,
|
||||||
$_SESSION['pending_login_user'] = $username;
|
'last_attempt' => time()
|
||||||
$_SESSION['pending_login_secret'] = $user['totp_secret'];
|
];
|
||||||
echo json_encode([
|
AuthModel::saveFailedAttempts($attemptsFile, $failed);
|
||||||
"totp_required" => true,
|
|
||||||
"message" => "TOTP code required"
|
|
||||||
]);
|
|
||||||
exit();
|
|
||||||
} else {
|
|
||||||
$tfa = new \RobThree\Auth\TwoFactorAuth(
|
|
||||||
new GoogleChartsQrCodeProvider(),
|
|
||||||
'FileRise',
|
|
||||||
6,
|
|
||||||
30,
|
|
||||||
Algorithm::Sha1
|
|
||||||
);
|
|
||||||
$providedCode = trim($data['totp_code']);
|
|
||||||
if (!$tfa->verifyCode($user['totp_secret'], $providedCode)) {
|
|
||||||
echo json_encode(["error" => "Invalid TOTP code"]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear failed attempts.
|
|
||||||
if (isset($failedAttempts[$ip])) {
|
|
||||||
unset($failedAttempts[$ip]);
|
|
||||||
AuthModel::saveFailedAttempts($attemptsFile, $failedAttempts);
|
|
||||||
}
|
|
||||||
|
|
||||||
session_regenerate_id(true);
|
|
||||||
$_SESSION["authenticated"] = true;
|
|
||||||
$_SESSION["username"] = $username;
|
|
||||||
$_SESSION["isAdmin"] = ($user['role'] === "1");
|
|
||||||
$_SESSION["folderOnly"] = loadUserPermissions($username);
|
|
||||||
|
|
||||||
// Handle "remember me"
|
|
||||||
if ($rememberMe) {
|
|
||||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
|
||||||
$tokenPersistent = bin2hex(random_bytes(32));
|
|
||||||
$expiry = time() + (30 * 24 * 60 * 60);
|
|
||||||
$persistentTokens = [];
|
|
||||||
if (file_exists($persistentTokensFile)) {
|
|
||||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
|
||||||
$decryptedContent = decryptData($encryptedContent, $GLOBALS['encryptionKey']);
|
|
||||||
$persistentTokens = json_decode($decryptedContent, true);
|
|
||||||
if (!is_array($persistentTokens)) {
|
|
||||||
$persistentTokens = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$persistentTokens[$tokenPersistent] = [
|
|
||||||
"username" => $username,
|
|
||||||
"expiry" => $expiry,
|
|
||||||
"isAdmin" => ($_SESSION["isAdmin"] === true)
|
|
||||||
];
|
|
||||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']);
|
|
||||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
|
||||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
|
||||||
setcookie('remember_me_token', $tokenPersistent, $expiry, '/', '', $secure, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
"status" => "ok",
|
|
||||||
"success" => "Login successful",
|
|
||||||
"isAdmin" => $_SESSION["isAdmin"],
|
|
||||||
"folderOnly" => $_SESSION["folderOnly"],
|
|
||||||
"username" => $_SESSION["username"]
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
// Record failed login attempt.
|
|
||||||
if (isset($failedAttempts[$ip])) {
|
|
||||||
$failedAttempts[$ip]['count']++;
|
|
||||||
$failedAttempts[$ip]['last_attempt'] = $currentTime;
|
|
||||||
} else {
|
|
||||||
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
|
|
||||||
}
|
|
||||||
AuthModel::saveFailedAttempts($attemptsFile, $failedAttempts);
|
|
||||||
$failedLogFile = USERS_DIR . 'failed_login.log';
|
|
||||||
$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);
|
http_response_code(401);
|
||||||
echo json_encode(["error" => "Invalid credentials"]);
|
echo json_encode(['error' => 'Invalid credentials']);
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if this account has TOTP, ask for it
|
||||||
|
if (! empty($user['totp_secret'])) {
|
||||||
|
$_SESSION['pending_login_user'] = $username;
|
||||||
|
$_SESSION['pending_login_secret'] = $user['totp_secret'];
|
||||||
|
echo json_encode(['totp_required' => true]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise clear rate‑limit & finish
|
||||||
|
if (isset($failed[$ip])) {
|
||||||
|
unset($failed[$ip]);
|
||||||
|
AuthModel::saveFailedAttempts($attemptsFile, $failed);
|
||||||
|
}
|
||||||
|
$this->finalizeLogin($username, $rememberMe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize an AJAX‐style login (form/basic/TOTP) by
|
||||||
|
* issuing the session, remember‑me cookie, and JSON payload.
|
||||||
|
*/
|
||||||
|
protected function finalizeLogin(string $username, bool $rememberMe): void
|
||||||
|
{
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['username'] = $username;
|
||||||
|
$_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1');
|
||||||
|
|
||||||
|
$perms = loadUserPermissions($username);
|
||||||
|
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||||
|
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||||
|
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||||
|
|
||||||
|
// remember‑me
|
||||||
|
if ($rememberMe) {
|
||||||
|
$tokFile = USERS_DIR . 'persistent_tokens.json';
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$expiry = time() + 30 * 24 * 60 * 60;
|
||||||
|
$all = [];
|
||||||
|
if (file_exists($tokFile)) {
|
||||||
|
$dec = decryptData(file_get_contents($tokFile), $GLOBALS['encryptionKey']);
|
||||||
|
$all = json_decode($dec, true) ?: [];
|
||||||
|
}
|
||||||
|
$all[$token] = [
|
||||||
|
'username' => $username,
|
||||||
|
'expiry' => $expiry,
|
||||||
|
'isAdmin' => $_SESSION['isAdmin']
|
||||||
|
];
|
||||||
|
file_put_contents(
|
||||||
|
$tokFile,
|
||||||
|
encryptData(json_encode($all, JSON_PRETTY_PRINT), $GLOBALS['encryptionKey']),
|
||||||
|
LOCK_EX
|
||||||
|
);
|
||||||
|
$secure = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
|
||||||
|
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'ok',
|
||||||
|
'success' => 'Login successful',
|
||||||
|
'isAdmin' => $_SESSION['isAdmin'],
|
||||||
|
'folderOnly' => $_SESSION['folderOnly'],
|
||||||
|
'readOnly' => $_SESSION['readOnly'],
|
||||||
|
'disableUpload' => $_SESSION['disableUpload'],
|
||||||
|
'username' => $username
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A version of finalizeLogin() that ends in a browser redirect
|
||||||
|
* (used for OIDC non‑AJAX flows).
|
||||||
|
*/
|
||||||
|
protected function finishBrowserLogin(string $username): void
|
||||||
|
{
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['username'] = $username;
|
||||||
|
$_SESSION['isAdmin'] = (AuthModel::getUserRole($username) === '1');
|
||||||
|
|
||||||
|
$perms = loadUserPermissions($username);
|
||||||
|
$_SESSION['folderOnly'] = $perms['folderOnly'] ?? false;
|
||||||
|
$_SESSION['readOnly'] = $perms['readOnly'] ?? false;
|
||||||
|
$_SESSION['disableUpload'] = $perms['disableUpload'] ?? false;
|
||||||
|
|
||||||
|
header('Location: /index.html');
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,51 +320,45 @@ class AuthController
|
|||||||
*
|
*
|
||||||
* @return void Outputs a JSON response with authentication details.
|
* @return void Outputs a JSON response with authentication details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function checkAuth(): void
|
public function checkAuth(): void
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$usersFile = USERS_DIR . USERS_FILE;
|
$usersFile = USERS_DIR . USERS_FILE;
|
||||||
// If the users file does not exist or is empty, signal setup mode.
|
|
||||||
|
// setup mode?
|
||||||
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
if (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '') {
|
||||||
error_log("checkAuth: users file not found or empty; entering setup mode.");
|
error_log("checkAuth: setup mode");
|
||||||
echo json_encode(["setup" => true]);
|
echo json_encode(['setup' => true]);
|
||||||
exit;
|
exit();
|
||||||
|
}
|
||||||
|
if (empty($_SESSION['authenticated'])) {
|
||||||
|
echo json_encode(['authenticated' => false]);
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the session is not authenticated, output false.
|
// TOTP enabled?
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
$totp = false;
|
||||||
echo json_encode(["authenticated" => false]);
|
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
exit;
|
$parts = explode(':', trim($line));
|
||||||
}
|
if ($parts[0] === $_SESSION['username'] && !empty($parts[3])) {
|
||||||
|
$totp = true;
|
||||||
// Retrieve the username from the session.
|
break;
|
||||||
$username = $_SESSION['username'] ?? '';
|
|
||||||
// Determine TOTP enabled by checking the users file.
|
|
||||||
$totp_enabled = false;
|
|
||||||
if ($username) {
|
|
||||||
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|
||||||
$parts = explode(':', trim($line));
|
|
||||||
if ($parts[0] === $username && isset($parts[3]) && trim($parts[3]) !== "") {
|
|
||||||
$totp_enabled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine admin status using AuthModel::getUserRole()
|
$isAdmin = ((int)AuthModel::getUserRole($_SESSION['username']) === 1);
|
||||||
$userRole = AuthModel::getUserRole($username);
|
$resp = [
|
||||||
$isAdmin = ((int)$userRole === 1);
|
'authenticated' => true,
|
||||||
|
'isAdmin' => $isAdmin,
|
||||||
$response = [
|
'totp_enabled' => $totp,
|
||||||
"authenticated" => true,
|
'username' => $_SESSION['username'],
|
||||||
"isAdmin" => $isAdmin,
|
'folderOnly' => $_SESSION['folderOnly'] ?? false,
|
||||||
"totp_enabled" => $totp_enabled,
|
'readOnly' => $_SESSION['readOnly'] ?? false,
|
||||||
"username" => $username,
|
'disableUpload' => $_SESSION['disableUpload'] ?? false
|
||||||
"folderOnly" => $_SESSION["folderOnly"] ?? false
|
|
||||||
];
|
];
|
||||||
echo json_encode($response);
|
echo json_encode($resp);
|
||||||
exit;
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -441,7 +459,11 @@ class AuthController
|
|||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $username;
|
$_SESSION["username"] = $username;
|
||||||
$_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1");
|
$_SESSION["isAdmin"] = (AuthModel::getUserRole($username) === "1");
|
||||||
$_SESSION["folderOnly"] = AuthModel::loadFolderPermission($username);
|
// load _all_ the permissions
|
||||||
|
$userPerms = loadUserPermissions($username);
|
||||||
|
$_SESSION["folderOnly"] = $userPerms["folderOnly"] ?? false;
|
||||||
|
$_SESSION["readOnly"] = $userPerms["readOnly"] ?? false;
|
||||||
|
$_SESSION["disableUpload"] = $userPerms["disableUpload"] ?? false;
|
||||||
|
|
||||||
header("Location: /index.html");
|
header("Location: /index.html");
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -1301,7 +1301,18 @@ class FileController {
|
|||||||
|
|
||||||
// Delegate deletion to the model.
|
// Delegate deletion to the model.
|
||||||
$result = FileModel::deleteTrashFiles($filesToDelete);
|
$result = FileModel::deleteTrashFiles($filesToDelete);
|
||||||
echo json_encode($result);
|
|
||||||
|
// Build a human‑friendly success or error message
|
||||||
|
if (!empty($result['deleted'])) {
|
||||||
|
$count = count($result['deleted']);
|
||||||
|
$msg = "Trash item" . ($count===1 ? "" : "s") . " deleted: " . implode(", ", $result['deleted']);
|
||||||
|
echo json_encode(["success" => $msg]);
|
||||||
|
} elseif (!empty($result['error'])) {
|
||||||
|
echo json_encode(["error" => $result['error']]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(["success" => "No items to delete."]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user